feat: add frontend project to repo
This commit is contained in:
128
.gitignore
vendored
128
.gitignore
vendored
@@ -1,102 +1,3 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template before PyInstaller builds the exe,
|
||||
# so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, you may want to ignore -lock files containing production packages.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
@@ -106,38 +7,9 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Profiling data
|
||||
.profiling/
|
||||
|
||||
# vscode settings
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/rest-client.env.json
|
||||
!.vscode/http
|
||||
!.vscode/sql
|
||||
|
||||
# FastAPI specifics
|
||||
# .env files used for storing environment variables should not be committed to version control
|
||||
# `db` folder might be used for database files such as SQLite which should not be checked into version control
|
||||
# db/
|
||||
|
||||
127
backend/.gitignore
vendored
Normal file
127
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template before PyInstaller builds the exe,
|
||||
# so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, you may want to ignore -lock files containing production packages.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Profiling data
|
||||
.profiling/
|
||||
|
||||
# FastAPI specifics
|
||||
# .env files used for storing environment variables should not be committed to version control
|
||||
# `db` folder might be used for database files such as SQLite which should not be checked into version control
|
||||
# db/
|
||||
@@ -2,7 +2,7 @@ from databases import Database
|
||||
from sqlalchemy import create_engine, MetaData
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
from app.config import settings
|
||||
from backend.config import settings
|
||||
|
||||
# for Alembic migrations and sync operations
|
||||
engine = create_engine(settings.database_url)
|
||||
@@ -1,8 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.config import settings
|
||||
from app.db import database, Base, engine
|
||||
from app.routes import shortener_router
|
||||
from backend.config import settings
|
||||
from backend.db import database, Base, engine
|
||||
from backend.routes import shortener_router
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -18,4 +18,4 @@ async def startup():
|
||||
async def shutdown():
|
||||
await database.disconnect()
|
||||
|
||||
app.include_router(shortener_router)
|
||||
app.include_router(shortener_router)
|
||||
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
from app.db.database import Base
|
||||
from backend.db.database import Base
|
||||
|
||||
class Url(Base):
|
||||
__tablename__ = "urls"
|
||||
@@ -1,11 +1,11 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.db import database
|
||||
from app.exceptions import ShortcodeNotFound, ShortcodeConflict
|
||||
from app.utils import encoder
|
||||
from app.schemas import UrlPayload
|
||||
from app.services import UrlService
|
||||
from backend.db import database
|
||||
from backend.exceptions import ShortcodeNotFound, ShortcodeConflict
|
||||
from backend.utils import encoder
|
||||
from backend.schemas import UrlPayload
|
||||
from backend.services import UrlService
|
||||
|
||||
router = APIRouter()
|
||||
url_service = UrlService(db=database)
|
||||
@@ -1,9 +1,9 @@
|
||||
from sqlalchemy import select, insert
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.utils import encoder
|
||||
from app.models import Url
|
||||
from app.exceptions import ShortcodeConflict, ShortcodeNotFound
|
||||
from backend.utils import encoder
|
||||
from backend.models import Url
|
||||
from backend.exceptions import ShortcodeConflict, ShortcodeNotFound
|
||||
|
||||
class UrlService:
|
||||
def __init__(self, db):
|
||||
@@ -1,6 +1,6 @@
|
||||
from hashids import Hashids
|
||||
|
||||
from app.config import settings
|
||||
from backend.config import settings
|
||||
|
||||
class ShortCodeEncoder:
|
||||
def __init__(self, salt=None, alphabet=None, min_length=6):
|
||||
50
frontend/.gitignore
vendored
Normal file
50
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Node dependencies
|
||||
node_modules/
|
||||
|
||||
# Production build output
|
||||
build/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# dotenv environment variables
|
||||
.env
|
||||
.env.*.local
|
||||
|
||||
# Editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Tailwind CSS generated output (if not purged correctly)
|
||||
*.css.map
|
||||
|
||||
# Optional build artifacts
|
||||
dist/
|
||||
temp/
|
||||
.cache/
|
||||
.next/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Storybook files
|
||||
.out/
|
||||
.storybook-ou
|
||||
46
frontend/README.md
Normal file
46
frontend/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
17762
frontend/package-lock.json
generated
Normal file
17762
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
frontend/package.json
Normal file
53
frontend/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "minxa-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
51
frontend/public/index.html
Normal file
51
frontend/public/index.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- ✅ App Title -->
|
||||
<title>minxa.lol - Shorten URLs with Style</title>
|
||||
|
||||
<!-- ✅ Responsive -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!-- ✅ Description -->
|
||||
<meta name="description" content="minxa.lol is a fun and simple URL shortener. Paste a long link and get a short, stylish one!" />
|
||||
|
||||
<!-- ✅ Theme color -->
|
||||
<meta name="theme-color" content="#E9D5FF" /> <!-- light purple -->
|
||||
|
||||
<!-- ✅ Open Graph (for social sharing) -->
|
||||
<meta property="og:title" content="minxa.lol - Stylish URL Shortener" />
|
||||
<meta property="og:description" content="Create short, playful links with minxa.lol" />
|
||||
<meta property="og:url" content="https://minxa.lol" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/logo.png" />
|
||||
|
||||
<!-- ✅ Favicon -->
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
|
||||
<!-- ✅ Google Fonts (if using) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- ✅ PWA support -->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<!-- Default CRA meta -->
|
||||
<meta name="robots" content="index, follow" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/public/logo.png
Normal file
BIN
frontend/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
26
frontend/public/manifest.json
Normal file
26
frontend/public/manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"short_name": "minxa.lol",
|
||||
"name": "minxa.lol - URL Shortener",
|
||||
"description": "A fun and minimalist URL shortener",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#E9D5FF",
|
||||
"background_color": "#FFFFFF"
|
||||
}
|
||||
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
frontend/src/App.css
Normal file
38
frontend/src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
9
frontend/src/App.tsx
Normal file
9
frontend/src/App.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import AppRouter from './routes/AppRouter';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return <AppRouter />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
8
frontend/src/api/url/types.ts
Normal file
8
frontend/src/api/url/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export interface ShortenUrlRequest {
|
||||
longUrl: string;
|
||||
}
|
||||
|
||||
export interface ShortenUrlResponse {
|
||||
shortCode: string;
|
||||
}
|
||||
23
frontend/src/api/url/urlApi.ts
Normal file
23
frontend/src/api/url/urlApi.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ShortenUrlRequest, ShortenUrlResponse } from "./types";
|
||||
|
||||
export async function shortenUrlApi(
|
||||
payload: ShortenUrlRequest
|
||||
): Promise<ShortenUrlResponse> {
|
||||
/* TODO
|
||||
|
||||
const response = await fetch('https://api.minxa.lol/api/v1/url', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to shorten URL');
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
return {
|
||||
shortCode: 'Ux5vy' // Dummy value return
|
||||
}
|
||||
}
|
||||
5
frontend/src/app/hooks.ts
Normal file
5
frontend/src/app/hooks.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
11
frontend/src/app/store.ts
Normal file
11
frontend/src/app/store.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import urlReducer from '../features/url/urlSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
url: urlReducer
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
6
frontend/src/features/url/types.ts
Normal file
6
frontend/src/features/url/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
export interface UrlState {
|
||||
shortUrl: string;
|
||||
status: 'idle' | 'loading' | 'succeeded' | 'failed';
|
||||
error: string | null;
|
||||
}
|
||||
53
frontend/src/features/url/urlSlice.ts
Normal file
53
frontend/src/features/url/urlSlice.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from '../../app/store';
|
||||
import { shortenUrlApi } from '../../api/url/urlApi';
|
||||
import { UrlState } from './types';
|
||||
|
||||
export const shortenUrl = createAsyncThunk(
|
||||
'url/shortenUrl',
|
||||
async (longUrl: string) => {
|
||||
const data = await shortenUrlApi({ longUrl });
|
||||
return `https://minxa.lol/${data.shortCode}`;
|
||||
}
|
||||
);
|
||||
|
||||
const initialState: UrlState = {
|
||||
shortUrl: '',
|
||||
status: 'idle',
|
||||
error: null,
|
||||
};
|
||||
|
||||
const urlSlice = createSlice({
|
||||
name: 'url',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearShortUrl(state) {
|
||||
state.shortUrl = '';
|
||||
state.status = 'idle';
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(shortenUrl.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
state.shortUrl = '';
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(shortenUrl.fulfilled, (state, action: PayloadAction<string>) => {
|
||||
state.status = 'succeeded';
|
||||
state.shortUrl = action.payload;
|
||||
})
|
||||
.addCase(shortenUrl.rejected, (state, action) => {
|
||||
state.status = 'failed';
|
||||
state.error = action.error.message || 'Something went wrong';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearShortUrl } = urlSlice.actions;
|
||||
export const selectShortUrl = (state: RootState) => state.url.shortUrl;
|
||||
export const selectUrlStatus = (state: RootState) => state.url.status;
|
||||
export const selectUrlError = (state: RootState) => state.url.error;
|
||||
|
||||
export default urlSlice.reducer;
|
||||
3
frontend/src/index.css
Normal file
3
frontend/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
13
frontend/src/index.tsx
Normal file
13
frontend/src/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './app/store';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
89
frontend/src/pages/Home.tsx
Normal file
89
frontend/src/pages/Home.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../app/hooks';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import {
|
||||
shortenUrl,
|
||||
selectShortUrl,
|
||||
selectUrlStatus,
|
||||
selectUrlError,
|
||||
clearShortUrl,
|
||||
} from '../features/url/urlSlice';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
/* component level state */
|
||||
const [longUrl, setLongUrl] = useState('');
|
||||
|
||||
/* global state */
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const shortUrl = useAppSelector(selectShortUrl);
|
||||
const status = useAppSelector(selectUrlStatus);
|
||||
const error = useAppSelector(selectUrlError);
|
||||
|
||||
/* methods */
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && longUrl.trim() !== '') {
|
||||
dispatch(shortenUrl(longUrl));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (shortUrl) {
|
||||
copyToClipboard(shortUrl)
|
||||
.then(() => console.log('Copied!'))
|
||||
.catch((err) => console.error('Copy failed', err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-purple-100">
|
||||
<div className="text-center space-y-6">
|
||||
<h1 className="text-5xl text-orange-500 font-pacifico">minxa.lol</h1>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your long URL here"
|
||||
value={longUrl}
|
||||
onChange={(e) => setLongUrl(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="w-80 p-3 rounded-md text-lg border border-gray-300 shadow-sm focus:outline-none focus:ring-2 focus:ring-orange-400"
|
||||
/>
|
||||
|
||||
{/* Loading State */}
|
||||
{status === 'loading' && (
|
||||
<p className="text-gray-600 text-sm">Shortening your URL...</p>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{status === 'failed' && (
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Short URL Display */}
|
||||
{status === 'succeeded' && shortUrl && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-green-700 font-medium text-lg">
|
||||
Your short URL:
|
||||
<a
|
||||
href={shortUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline ml-2"
|
||||
>
|
||||
{shortUrl}
|
||||
</a>
|
||||
</p>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition">
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
1
frontend/src/react-app-env.d.ts
vendored
Normal file
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
frontend/src/routes/AppRouter.tsx
Normal file
15
frontend/src/routes/AppRouter.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import Home from '../pages/Home';
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
{/* Add more routes as needed */}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default AppRouter;
|
||||
36
frontend/src/utils/clipboard.ts
Normal file
36
frontend/src/utils/clipboard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
export function copyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// ✅ Modern way
|
||||
return navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// 🚨 Fallback for insecure context or unsupported browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
// Avoid scrolling to bottom
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
textArea.style.width = '2em';
|
||||
textArea.style.height = '2em';
|
||||
textArea.style.padding = '0';
|
||||
textArea.style.border = 'none';
|
||||
textArea.style.outline = 'none';
|
||||
textArea.style.boxShadow = 'none';
|
||||
textArea.style.background = 'transparent';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
15
frontend/tailwind.config.js
Normal file
15
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
pacifico: ['"Pacifico"', 'cursive'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user