From 145008d35101ed274db234625606e5a9571288f1 Mon Sep 17 00:00:00 2001 From: Fingertips Date: Thu, 12 Sep 2024 20:04:58 +0800 Subject: [PATCH] Create header Implement themes and routes --- air.toml | 2 +- client/package-lock.json | 72 +++++++++++++++++++- client/package.json | 5 +- client/src/App.tsx | 13 +++- client/src/assets/key.svg | 3 + client/src/components/header/index.tsx | 15 ++++ client/src/components/header/logo.tsx | 22 ++++++ client/src/components/header/toggle-mode.tsx | 26 +++++++ client/src/constants/assets.ts | 3 + client/src/constants/routes.ts | 8 +++ client/src/index.css | 40 +++++++++++ client/src/lib/hooks/use-theme.tsx | 21 ++++++ client/src/lib/providers/theme-provider.tsx | 69 +++++++++++++++++++ client/src/main.tsx | 9 ++- client/src/routes/root/page.tsx | 9 +++ client/tailwind.config.js | 34 ++++++--- client/tsconfig.app.json | 7 +- client/vite.config.ts | 12 +++- configs/email_config_test.go | 7 -- configs/setup_cors_test.go | 4 +- controllers/auth_controller.go | 2 +- utils/email_service_test.go | 2 - utils/session_token_test.go | 17 +---- 23 files changed, 353 insertions(+), 49 deletions(-) create mode 100644 client/src/assets/key.svg create mode 100644 client/src/components/header/index.tsx create mode 100644 client/src/components/header/logo.tsx create mode 100644 client/src/components/header/toggle-mode.tsx create mode 100644 client/src/constants/assets.ts create mode 100644 client/src/constants/routes.ts create mode 100644 client/src/lib/hooks/use-theme.tsx create mode 100644 client/src/lib/providers/theme-provider.tsx create mode 100644 client/src/routes/root/page.tsx diff --git a/air.toml b/air.toml index 476a0b9..46ce850 100644 --- a/air.toml +++ b/air.toml @@ -4,6 +4,6 @@ tmp_dir = "tmp" [build] bin = "main" cmd = "go build -o {{.Output}} {{.Input}}" - exclude = ["tmp/*", "frontend/*"] + exclude = ["tmp/*", "client/*"] include = ["**/*.go"] ignore = ["tmp/*"] \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 7641fbe..d6845f4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "lucide-react": "^0.440.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2" }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/node": "^22.5.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", @@ -734,6 +737,15 @@ "node": ">=14" } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.21.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", @@ -1191,6 +1203,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -2703,6 +2725,15 @@ "dev": true, "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.440.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.440.0.tgz", + "integrity": "sha512-FW6Z0IJ/WVG968jscSiPXvQWo5B2DddynpgDMyGmTqnBo7pxzJrFMFMChM/d5YEsdHiNoLq0GIZJn14tH70GYQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3225,6 +3256,38 @@ "react": "^18.3.1" } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -3726,6 +3789,13 @@ } } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/client/package.json b/client/package.json index 9211e0e..c85f4ef 100644 --- a/client/package.json +++ b/client/package.json @@ -15,11 +15,14 @@ "preview": "vite preview" }, "dependencies": { + "lucide-react": "^0.440.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2" }, "devDependencies": { "@eslint/js": "^9.9.0", + "@types/node": "^22.5.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index fdc335e..3b78955 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,16 @@ +import { Route, Routes } from "react-router-dom"; + +import { AppRoutes } from "@/constants/routes"; +import { Header } from "@/components/header"; +import RootPage from "@/routes/root/page"; + function App() { return ( -
-

Hello world!

+
+
+ + } /> +
); } diff --git a/client/src/assets/key.svg b/client/src/assets/key.svg new file mode 100644 index 0000000..48b7567 --- /dev/null +++ b/client/src/assets/key.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/src/components/header/index.tsx b/client/src/components/header/index.tsx new file mode 100644 index 0000000..5a3b4e6 --- /dev/null +++ b/client/src/components/header/index.tsx @@ -0,0 +1,15 @@ +import { ToggleMode } from "./toggle-mode"; +import { Logo } from "./logo"; + +const Header = () => { + return ( +
+ +
+ ); +}; + +export { Header }; diff --git a/client/src/components/header/logo.tsx b/client/src/components/header/logo.tsx new file mode 100644 index 0000000..f2ef3bc --- /dev/null +++ b/client/src/components/header/logo.tsx @@ -0,0 +1,22 @@ +import { Link } from "react-router-dom"; + +import { AppRoutes } from "@/constants/routes"; +import { KEY } from "@/constants/assets"; + +const Logo = () => { + return ( +

+ + Key Logo +
+ + Go + + React Auth +
+ +

+ ); +}; + +export { Logo }; diff --git a/client/src/components/header/toggle-mode.tsx b/client/src/components/header/toggle-mode.tsx new file mode 100644 index 0000000..5b3024a --- /dev/null +++ b/client/src/components/header/toggle-mode.tsx @@ -0,0 +1,26 @@ +import { Moon, Sun } from "lucide-react"; + +import { Theme, useTheme } from "@/lib/hooks/use-theme"; + +const ToggleMode = () => { + const { theme, setTheme } = useTheme(); + + const onClick = () => { + if (theme === Theme.Light) { + setTheme(Theme.Dark); + } else { + setTheme(Theme.Light); + } + }; + + return ( + + ); +}; + +export { ToggleMode }; diff --git a/client/src/constants/assets.ts b/client/src/constants/assets.ts new file mode 100644 index 0000000..1581dc8 --- /dev/null +++ b/client/src/constants/assets.ts @@ -0,0 +1,3 @@ +import key from "@/assets/key.svg"; + +export const KEY = key; diff --git a/client/src/constants/routes.ts b/client/src/constants/routes.ts new file mode 100644 index 0000000..13f150e --- /dev/null +++ b/client/src/constants/routes.ts @@ -0,0 +1,8 @@ +export enum AppRoutes { + Root = "/", + SignUp = "/sign-up", + SignIn = "/sign-in", + ForgotPassword = "/forgot-password", + ResetPassword = "/reset-password", + VerifyEmail = "/verify-email", +} diff --git a/client/src/index.css b/client/src/index.css index a0d6f44..f8c48cf 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -5,17 +5,57 @@ @tailwind utilities; @layer base { + :root { + --foreground: 6 35 31; + --background: 242 253 251; + --primary: 38 220 212; + --secondary: 235 158 132; + --secondary-fade: rgba(235, 158, 132, 0.2); + --accent: 187 230 96; + } + + .dark { + --foreground: 220 249 245; + --background: 2 13 11; + --primary: 35 215 206; + --secondary: 123 46 20; + --secondary-fade: rgba(123, 46, 20, 0.2); + --accent: 116 159 25; + } + * { @apply m-0 p-0 scroll-smooth box-border; } + html, body { @apply bg-background text-foreground font-poppins; } + + ::-webkit-scrollbar { + width: 4px; + } + ::-webkit-scrollbar-track { + background-color: var(--secondary-fade); + opacity: 0.2; + } + ::-webkit-scrollbar-thumb { + background-color: var(--secondary); + opacity: 0.8; + border-radius: 9999px; + transition: all 5s ease-out; + } + ::-webkit-scrollbar-thumb:hover { + background-color: var(--secondary); + opacity: 1; + } } @layer utilities { .flex-center { @apply flex items-center justify-center; } + .flex-between { + @apply flex items-center justify-between; + } } diff --git a/client/src/lib/hooks/use-theme.tsx b/client/src/lib/hooks/use-theme.tsx new file mode 100644 index 0000000..7dee5c7 --- /dev/null +++ b/client/src/lib/hooks/use-theme.tsx @@ -0,0 +1,21 @@ +import { useContext } from "react"; + +import { ThemeProviderContext } from "@/lib/providers/theme-provider"; + +export enum Theme { + System = "system", + Light = "light", + Dark = "dark", +} + +const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + + return context; +}; + +export { useTheme }; diff --git a/client/src/lib/providers/theme-provider.tsx b/client/src/lib/providers/theme-provider.tsx new file mode 100644 index 0000000..183acf4 --- /dev/null +++ b/client/src/lib/providers/theme-provider.tsx @@ -0,0 +1,69 @@ +import { createContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +const ThemeProvider = ({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) => { + const [theme, setTheme] = + useState(() => localStorage.getItem(storageKey) as Theme) || + defaultTheme; + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + + setTheme(systemTheme); + + return; + } + + root.classList.add(theme); + }, [theme, setTheme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +}; + +export { ThemeProvider, ThemeProviderContext }; diff --git a/client/src/main.tsx b/client/src/main.tsx index 615b97c..23b7314 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,11 +1,18 @@ +import { BrowserRouter } from "react-router-dom"; import { createRoot } from "react-dom/client"; import { StrictMode } from "react"; +import { ThemeProvider } from "@/lib/providers/theme-provider.tsx"; + import App from "./App.tsx"; import "./index.css"; createRoot(document.getElementById("root")!).render( - + + + + + ); diff --git a/client/src/routes/root/page.tsx b/client/src/routes/root/page.tsx new file mode 100644 index 0000000..9d88029 --- /dev/null +++ b/client/src/routes/root/page.tsx @@ -0,0 +1,9 @@ +const RootPage = () => { + return ( +
+

Root Page

+
+ ); +}; + +export default RootPage; diff --git a/client/tailwind.config.js b/client/tailwind.config.js index f86f21f..35e4da2 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -7,17 +7,29 @@ export default { poppins: ["Poppins", "sans-serif"], }, colors: { - foreground: "#06231F", - background: "#F2FDFB", - primary: "#26DCD4", - secondary: "#EB9E84", - accent: "#BBE660", - - "dark-foreground": "#DCF9F5", - "dark-background": "#020D0B", - "dark-primary": "#23D7CE", - "dark-secondary": "#7B2E14", - "dark-accent": "#749F19", + foreground: "rgb(var(--foreground))", + background: "rgb(var(--background))", + primary: "rgb(var(--primary))", + secondary: "rgb(var(--secondary))", + accent: "rgb(var(--accent))", + }, + dropShadow: { + "foreground-glow": [ + "0 0px 25px rgb(var(--foreground))", + "0 0px 50px rgb(var(--foreground))", + ], + "primary-glow": [ + "0 0px 25px rgb(var(--primary))", + "0 0px 50px rgb(var(--primary))", + ], + "secondary-glow": [ + "0 0px 25px rgb(var(--secondary))", + "0 0px 50px rgb(var(--secondary))", + ], + "accent-glow": [ + "0 0px 25px rgb(var(--accent))", + "0 0px 50px rgb(var(--accent))", + ], }, }, }, diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index f0a2350..44117e0 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -18,7 +18,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } }, "include": ["src"] } diff --git a/client/vite.config.ts b/client/vite.config.ts index 861b04b..f89b54d 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,13 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; +import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, +}); diff --git a/configs/email_config_test.go b/configs/email_config_test.go index 0486c50..3b32bdf 100644 --- a/configs/email_config_test.go +++ b/configs/email_config_test.go @@ -3,18 +3,11 @@ package configs import ( "bytes" "log" - "os" "testing" ) func TestConfigureEmail(t *testing.T) { - email := "EMAIL" - pass := "EMAIL_APP_PASSWORD" - t.Run("ConfigureEmail_Success", func(t *testing.T) { - os.Setenv(email, "test@example.com") - os.Setenv(pass, "password") - var buf bytes.Buffer log.SetOutput(&buf) diff --git a/configs/setup_cors_test.go b/configs/setup_cors_test.go index e0c4ca6..7999293 100644 --- a/configs/setup_cors_test.go +++ b/configs/setup_cors_test.go @@ -11,9 +11,7 @@ import ( func TestSetupCORS(t *testing.T) { envName := "CLIENT_URL" - testOrigin := "http://example.com" - - os.Setenv(envName, testOrigin) + testOrigin := os.Getenv(envName) app := fiber.New() SetupCORS(app) diff --git a/controllers/auth_controller.go b/controllers/auth_controller.go index ed64ff3..de96275 100644 --- a/controllers/auth_controller.go +++ b/controllers/auth_controller.go @@ -19,7 +19,7 @@ func SignUp(c fiber.Ctx) error { Password string `json:"password"` } if err := c.Bind().JSON(&data); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Fields are either invalid or missing"}) } user := models.User{ diff --git a/utils/email_service_test.go b/utils/email_service_test.go index 5411d80..3aeba0f 100644 --- a/utils/email_service_test.go +++ b/utils/email_service_test.go @@ -30,8 +30,6 @@ func TestEmailService(t *testing.T) { }) t.Run("SendEmailRequestResetPassword_Test", func(t *testing.T) { - os.Setenv("CLIENT_URL", "https://example.com") - if err := SendEmailRequestResetPassword(toEmail, "reset-token"); err != nil { t.Errorf("Ërror sending reset password request : %v", err) } else { diff --git a/utils/session_token_test.go b/utils/session_token_test.go index 2039a12..f340f3e 100644 --- a/utils/session_token_test.go +++ b/utils/session_token_test.go @@ -29,25 +29,12 @@ type JWTPayload struct { } func TestToken(t *testing.T) { - envClientName := "CLIENT_URL" - envSecretName := "SECRET_KEY" - envENVName := "ENV" - - // Set dummy client url - client := "http://client.com" - os.Setenv(envClientName, client) - - // Set dummy secret key - secret := "secret" - os.Setenv(envSecretName, secret) - // Credentials for jwt headers id := "@asd123" username := "test" - // Set dummy env mode - env := "development" - os.Setenv(envENVName, env) + envSecretName := "SECRET_KEY" + secret := os.Getenv(envSecretName) t.Run("GenerateJWTToken_Validate", func(t *testing.T) { token, err := GenerateJWTToken(id, username)