Skip to content

Commit 823fdce

Browse files
authored
Merge pull request #3 from jashgopani/release/1.0.0
Release/1.0.0
2 parents de2302e + b1c8b1a commit 823fdce

27 files changed

+5103
-0
lines changed

.prettierrc.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"arrowParens": "always",
3+
"jsxSingleQuote": true,
4+
"printWidth": 100,
5+
"proseWrap": "always",
6+
"bracketSameLine": true,
7+
"useTabs": false,
8+
"tabWidth": 2
9+
}

Backend/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ java {
1515

1616
repositories {
1717
mavenCentral()
18+
maven { url 'https://jitpack.io' }
1819
}
1920

2021
dependencies {
2122
implementation 'org.springframework.boot:spring-boot-starter-web'
2223
testImplementation 'org.springframework.boot:spring-boot-starter-test'
2324
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
25+
implementation 'com.soundicly:jnanoid-enhanced:main-SNAPSHOT'
2426
}
2527

2628
tasks.named('test') {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.jashgopani.urlshortner.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.servlet.config.annotation.CorsRegistry;
6+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
7+
8+
@Configuration
9+
public class WebConfig implements WebMvcConfigurer {
10+
11+
@Bean
12+
public WebMvcConfigurer corsConfigurer() {
13+
return new WebMvcConfigurer() {
14+
@Override
15+
public void addCorsMappings(CorsRegistry registry) {
16+
registry.addMapping("/**")
17+
.allowedOriginPatterns("*")
18+
.allowedMethods("*")
19+
.allowedHeaders("*")
20+
.allowCredentials(false);
21+
}
22+
};
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.jashgopani.urlshortner.slug.controller;
2+
3+
import org.springframework.web.bind.annotation.RestController;
4+
import org.springframework.web.servlet.view.RedirectView;
5+
6+
import com.jashgopani.urlshortner.slug.model.Slug;
7+
import com.jashgopani.urlshortner.slug.service.SlugService;
8+
import com.jashgopani.urlshortner.slug.utils.SlugConstants;
9+
10+
import jakarta.servlet.http.HttpServletResponse;
11+
12+
import java.nio.charset.MalformedInputException;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Objects;
17+
18+
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.http.HttpStatus;
20+
import org.springframework.http.HttpStatusCode;
21+
import org.springframework.http.ResponseEntity;
22+
import org.springframework.web.bind.annotation.GetMapping;
23+
import org.springframework.web.bind.annotation.PathVariable;
24+
import org.springframework.web.bind.annotation.PostMapping;
25+
import org.springframework.web.bind.annotation.RequestParam;
26+
import org.springframework.web.bind.annotation.RequestBody;
27+
28+
@RestController
29+
public class SlugController {
30+
31+
@Autowired
32+
private SlugService slugService;
33+
34+
@GetMapping("/")
35+
public String index() {
36+
return "URL Shortner is running!";
37+
}
38+
39+
@PostMapping("/")
40+
public ResponseEntity<?> shortenURL(@RequestParam("url") String url) {
41+
try {
42+
Slug slug = slugService.generateSlug(url);
43+
return ResponseEntity.status(HttpStatus.CREATED).body(slug);
44+
} catch (Exception e) {
45+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(getErrorMessage(SlugConstants.ERROR_INVALID_URL));
46+
}
47+
}
48+
49+
@GetMapping("/{slug}")
50+
public RedirectView redirectToURL(@PathVariable String slug) {
51+
try {
52+
Slug slugObj = slugService.getSlug(slug);
53+
if (Objects.isNull(slugObj)) {
54+
throw new NullPointerException();
55+
}
56+
return new RedirectView(slugObj.getUrl());
57+
} catch (Exception e) {
58+
59+
RedirectView redirectView = new RedirectView("/error");
60+
redirectView.setStatusCode(HttpStatus.NOT_FOUND);
61+
return redirectView;
62+
}
63+
64+
}
65+
66+
@GetMapping("/slugs")
67+
public List<Slug> findAllSlugs() {
68+
return slugService.findAll();
69+
}
70+
71+
private Map<String, String> getErrorMessage(String message) {
72+
Map<String, String> error = new HashMap<>();
73+
error.put("error", message);
74+
return error;
75+
}
76+
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.jashgopani.urlshortner.slug.model;
2+
3+
/**
4+
* Model for the slug resource
5+
*/
6+
public class Slug {
7+
private String id;
8+
private String url;
9+
10+
public Slug(String id, String url) {
11+
this.id = id;
12+
this.url = url;
13+
}
14+
15+
public String getId() {
16+
return id;
17+
}
18+
19+
public String getUrl() {
20+
return url;
21+
}
22+
23+
public void setId(String id) {
24+
this.id = id;
25+
}
26+
27+
public void setUrl(String url) {
28+
this.url = url;
29+
}
30+
31+
@Override
32+
public String toString() {
33+
return "Slug [id=" + id + ", url=" + url + "]";
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.jashgopani.urlshortner.slug.repository;
2+
3+
import java.util.List;
4+
5+
import com.jashgopani.urlshortner.slug.model.Slug;
6+
7+
public interface ISlugRepository {
8+
9+
public Slug findById(String id);
10+
11+
public List<Slug> findAll();
12+
13+
public void save(Slug slug);
14+
15+
public void save(List<Slug> slug);
16+
17+
public Slug delete(String id);
18+
19+
public void update(Slug slug);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.jashgopani.urlshortner.slug.repository;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Objects;
6+
7+
import org.springframework.stereotype.Repository;
8+
9+
import com.jashgopani.urlshortner.slug.model.Slug;
10+
11+
/**
12+
* Class to handle the database operations for the slug resource
13+
*/
14+
@Repository
15+
public class SlugRepository implements ISlugRepository {
16+
HashMap<String, Slug> db;
17+
18+
public SlugRepository() {
19+
db = new HashMap<>();
20+
}
21+
22+
@Override
23+
public Slug delete(String id) {
24+
if (!db.containsKey(id)) {
25+
throw new IllegalArgumentException("Slug with id " + id + " does not exist");
26+
}
27+
return db.remove(id);
28+
}
29+
30+
@Override
31+
public List<Slug> findAll() {
32+
return db.values().stream().toList();
33+
}
34+
35+
@Override
36+
public Slug findById(String id) {
37+
return db.get(id);
38+
}
39+
40+
@Override
41+
public void save(Slug slug) {
42+
if (db.containsKey(slug.getId())) {
43+
throw new IllegalArgumentException("Slug with id " + slug.getId() + " already exists");
44+
}
45+
db.put(slug.getId(), slug);
46+
}
47+
48+
@Override
49+
public void save(List<Slug> slug) {
50+
slug.forEach(this::save);
51+
}
52+
53+
@Override
54+
public void update(Slug slug) {
55+
if (!db.containsKey(slug.getId())) {
56+
throw new IllegalArgumentException("Slug with id " + slug.getId() + " does not exist");
57+
}
58+
db.put(slug.getId(), slug);
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.jashgopani.urlshortner.slug.service;
2+
3+
import java.io.IOException;
4+
import java.net.MalformedURLException;
5+
import java.net.URL;
6+
import java.util.List;
7+
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.stereotype.Service;
11+
12+
import com.jashgopani.urlshortner.slug.model.Slug;
13+
import com.jashgopani.urlshortner.slug.repository.SlugRepository;
14+
import com.soundicly.jnanoidenhanced.jnanoid.NanoIdUtils;
15+
16+
/**
17+
* Class to handle the business logic for the slug resource
18+
*/
19+
@Service
20+
public class SlugService {
21+
22+
@Autowired
23+
private SlugRepository slugRepository;
24+
25+
@Value("${slug.length}")
26+
private int slugLength;
27+
28+
/**
29+
* Method to generate a random slug
30+
*
31+
* @return a random slug
32+
* @throws IOException
33+
* @throws MalformedURLException
34+
*/
35+
public Slug generateSlug(String url) throws MalformedURLException, IOException {
36+
new URL(url).openConnection().connect();// this will throw an error is URL is invalid
37+
38+
String id = NanoIdUtils.randomNanoId(slugLength);
39+
Slug slug = new Slug(id, url);
40+
slugRepository.save(slug);
41+
return slug;
42+
}
43+
44+
/**
45+
* Method to get a slug by its id
46+
*
47+
* @param id the id of the slug
48+
* @return the slug with the given id
49+
*/
50+
public Slug getSlug(String id) {
51+
return slugRepository.findById(id);
52+
}
53+
54+
55+
public List<Slug> findAll() {
56+
return slugRepository.findAll();
57+
}
58+
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.jashgopani.urlshortner.slug.utils;
2+
3+
public class SlugConstants {
4+
public static final String ERROR_INVALID_URL = "Provided URL is invalid. Please check the URL and try again.";
5+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
spring.application.name=urlshortner
2+
logging.level.org.springframework.web=DEBUG
3+
slug.length=11

Frontend/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# React + Vite
2+
3+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4+
5+
Currently, two official plugins are available:
6+
7+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

Frontend/eslint.config.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import react from 'eslint-plugin-react'
4+
import reactHooks from 'eslint-plugin-react-hooks'
5+
import reactRefresh from 'eslint-plugin-react-refresh'
6+
7+
export default [
8+
{ ignores: ['dist'] },
9+
{
10+
files: ['**/*.{js,jsx}'],
11+
languageOptions: {
12+
ecmaVersion: 2020,
13+
globals: globals.browser,
14+
parserOptions: {
15+
ecmaVersion: 'latest',
16+
ecmaFeatures: { jsx: true },
17+
sourceType: 'module',
18+
},
19+
},
20+
settings: { react: { version: '18.3' } },
21+
plugins: {
22+
react,
23+
'react-hooks': reactHooks,
24+
'react-refresh': reactRefresh,
25+
},
26+
rules: {
27+
...js.configs.recommended.rules,
28+
...react.configs.recommended.rules,
29+
...react.configs['jsx-runtime'].rules,
30+
...reactHooks.configs.recommended.rules,
31+
'react/jsx-no-target-blank': 'off',
32+
'react-refresh/only-export-components': [
33+
'warn',
34+
{ allowConstantExport: true },
35+
],
36+
},
37+
},
38+
]

Frontend/index.html

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link
6+
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
7+
rel="stylesheet"
8+
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
9+
crossorigin="anonymous" />
10+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
12+
<title>URL Shortner</title>
13+
</head>
14+
<body>
15+
<div id="root"></div>
16+
<script
17+
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
18+
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
19+
crossorigin="anonymous"></script>
20+
<script type="module" src="/src/main.jsx"></script>
21+
</body>
22+
</html>

0 commit comments

Comments
 (0)