diff --git a/.env b/.env new file mode 100644 index 0000000..5f32319 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +BROWSER=none \ No newline at end of file diff --git a/chickenscratch.txt b/chickenscratch.txt new file mode 100644 index 0000000..5b0d041 --- /dev/null +++ b/chickenscratch.txt @@ -0,0 +1,41 @@ + + + + + + + minxa.lol - Shorten URLs with Style + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 402c809..de01e43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,10 @@ "web-vitals": "^2.1.4" }, "devDependencies": { - "@types/react-router-dom": "^5.3.3" + "@types/react-router-dom": "^5.3.3", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } }, "node_modules/@adobe/css-tools": { @@ -3112,16 +3115,6 @@ } } }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3501,15 +3494,6 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", @@ -4716,12 +4700,12 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -6015,12 +5999,12 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-signature": { @@ -7550,6 +7534,15 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -8002,6 +7995,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -9268,9 +9270,9 @@ } }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", "license": "MIT", "funding": { "type": "opencollective", @@ -13952,6 +13954,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", @@ -14100,15 +14112,6 @@ "react-dom": ">=18" } }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/package.json b/package.json index 23d0175..f1efaf8 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ ] }, "devDependencies": { - "@types/react-router-dom": "^5.3.3" + "@types/react-router-dom": "^5.3.3", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/index.html b/public/index.html index aa069f2..e9719ce 100644 --- a/public/index.html +++ b/public/index.html @@ -2,29 +2,37 @@ - - - - - - - - + minxa.lol - Shorten URLs with Style - Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will - work correctly both with client-side routing and a non-root public URL. - Learn how to configure a non-root public URL by running `npm run build`. - --> - React App + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/src/api/url/types.ts b/src/api/url/types.ts new file mode 100644 index 0000000..8906275 --- /dev/null +++ b/src/api/url/types.ts @@ -0,0 +1,8 @@ + +export interface ShortenUrlRequest { + longUrl: string; +} + +export interface ShortenUrlResponse { + shortCode: string; +} \ No newline at end of file diff --git a/src/api/url/urlApi.ts b/src/api/url/urlApi.ts new file mode 100644 index 0000000..9ffb16b --- /dev/null +++ b/src/api/url/urlApi.ts @@ -0,0 +1,23 @@ +import { ShortenUrlRequest, ShortenUrlResponse } from "./types"; + +export async function shortenUrlApi( + payload: ShortenUrlRequest +): Promise { + /* 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 + } +} \ No newline at end of file diff --git a/src/app/hooks.ts b/src/app/hooks.ts new file mode 100644 index 0000000..e6f6c0d --- /dev/null +++ b/src/app/hooks.ts @@ -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 = useSelector; \ No newline at end of file diff --git a/src/app/store.ts b/src/app/store.ts index fb84cce..814d6eb 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,7 +1,9 @@ import { configureStore } from '@reduxjs/toolkit'; +import urlReducer from '../features/url/urlSlice'; export const store = configureStore({ reducer: { + url: urlReducer }, }); diff --git a/src/features/url/types.ts b/src/features/url/types.ts new file mode 100644 index 0000000..6ff4c82 --- /dev/null +++ b/src/features/url/types.ts @@ -0,0 +1,6 @@ + +export interface UrlState { + shortUrl: string; + status: 'idle' | 'loading' | 'succeeded' | 'failed'; + error: string | null; +} \ No newline at end of file diff --git a/src/features/url/urlSlice.ts b/src/features/url/urlSlice.ts new file mode 100644 index 0000000..7de2a43 --- /dev/null +++ b/src/features/url/urlSlice.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/index.css b/src/index.css index ec2585e..bd6213e 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,3 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index b07e7f0..a4ec3dc 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,8 +1,45 @@ -import React, {useState } from 'react'; +import React, { useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../app/hooks'; +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) => { + if (e.key === 'Enter' && longUrl.trim() !== '') { + dispatch(shortenUrl(longUrl)); + } + }; + return ( -
Hello World!
+
+
+

minxa.lol

+ 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" + /> +
+
); }; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..1f13ecb --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,ts,jsx,tsx}" + ], + theme: { + extend: { + fontFamily: { + pacifico: ['"Pacifico"', 'cursive'], + }, + }, + }, + plugins: [], +} +