├── src ├── style.css ├── App.js ├── index.js ├── PasswordScreen.js ├── EmailInput.js └── emailInput.css ├── readme-banner.png ├── renovate.json ├── postcss.config.mjs ├── lib └── utils.ts ├── components ├── theme-provider.tsx ├── AuthSuccess.tsx ├── DashboardUI.tsx ├── AuthFlow.tsx ├── EmailInput.tsx ├── UserNamePrompt.tsx ├── OtpVerification.tsx ├── authSuccess.css ├── otpVerification.css ├── userNamePrompt.css ├── emailInput.css └── dashboardUI.css ├── .gitignore ├── app ├── globals.css ├── page.tsx ├── layout.tsx └── dashboard │ └── page.tsx ├── components.json ├── public ├── placeholder-logo.png ├── placeholder.jpg ├── placeholder-user.jpg ├── index.html ├── placeholder-logo.svg └── placeholder.svg ├── middleware.ts ├── hooks ├── use-mobile.tsx └── use-toast.ts ├── tsconfig.json ├── README.md ├── next.config.mjs ├── package.json ├── styles └── globals.css └── tailwind.config.ts /src/style.css: -------------------------------------------------------------------------------- 1 | input { 2 | height: 30px; 3 | } 4 | 5 | .error { 6 | color: red; 7 | } 8 | -------------------------------------------------------------------------------- /readme-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/byos-sample-app/HEAD/readme-banner.png -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import AuthFlow from "./components/AuthFlow" 4 | 5 | export default function App() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>descope-sample-apps/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import "./style.css" 4 | 5 | import App from "./App" 6 | 7 | const rootElement = document.getElementById("root") 8 | const root = createRoot(rootElement) 9 | 10 | root.render( 11 | 12 | 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | ThemeProvider as NextThemesProvider, 6 | type ThemeProviderProps, 7 | } from 'next-themes' 8 | 9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # next.js 7 | /.next/ 8 | /out/ 9 | 10 | # production 11 | /build 12 | 13 | # debug 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | .pnpm-debug.log* 18 | 19 | # env files 20 | .env* 21 | 22 | # vercel 23 | .vercel 24 | 25 | # typescript 26 | *.tsbuildinfo 27 | next-env.d.ts -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --foreground-rgb: 0, 0, 0; 3 | --background-start-rgb: 214, 219, 220; 4 | --background-end-rgb: 255, 255, 255; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | padding: 0; 10 | margin: 0; 11 | } 12 | 13 | html, 14 | body { 15 | max-width: 100vw; 16 | overflow-x: hidden; 17 | height: 100%; 18 | } 19 | 20 | body { 21 | color: rgb(var(--foreground-rgb)); 22 | } 23 | 24 | a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | 29 | .error { 30 | color: red; 31 | } 32 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /public/placeholder-logo.png: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR�M��0PLTEZ? tRNS� �@��`P0p���w �IDATx��ؽJ3Q�7'��%�|?� ���E�l�7���(X�D������w`����[�*t����D���mD�}��4; ;�DDDDDDDDDDDD_�_İ��!�y�`�_�:�� ;Ļ�'|� ��;.I"����3*5����J�1�� �T��FI�� ��=��3܃�2~�b���0��U9\��]�4�#w0��Gt\&1 �?21,���o!e�m��ĻR�����5�� ؽAJ�9��R)�5�0.FFASaǃ�T�#|�K���I�������1� 4 | M������N"��$����G�V�T� ��T^^��A�$S��h(�������G]co"J׸^^�'�=���%� �W�6Ы�W��w�a�߇*�^^�YG�c���`'F����������������^5_�,�S�%IEND�B`� -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect } from "react" 4 | import { useSession } from "@descope/nextjs-sdk/client" 5 | import { useRouter } from "next/navigation" 6 | import AuthFlow from "@/components/AuthFlow" 7 | 8 | export default function Home() { 9 | const router = useRouter() 10 | const { isAuthenticated } = useSession() 11 | 12 | useEffect(() => { 13 | if (isAuthenticated) { 14 | router.push("/dashboard") 15 | } 16 | }, [isAuthenticated, router]) 17 | 18 | return ( 19 |
20 | 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from '@descope/nextjs-sdk/server' 2 | 3 | export default authMiddleware({ 4 | // The Descope project ID to use for authentication 5 | // Defaults to process.env.DESCOPE_PROJECT_ID 6 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, 7 | 8 | // The URL to redirect to if the user is not authenticated 9 | redirectUrl: "/", 10 | 11 | // An array of public routes that do not require authentication 12 | publicRoutes: ["/"], 13 | }) 14 | 15 | export const config = { 16 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 17 | } 18 | -------------------------------------------------------------------------------- /src/PasswordScreen.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | export default ({ onFormUpdate, onClick, onChange, errorText }) => { 4 | return ( 5 |
6 |
7 | 16 |
17 | 18 | {errorText &&

{errorText}

} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react" 2 | import { Inter } from "next/font/google" 3 | import { AuthProvider } from "@descope/nextjs-sdk" 4 | import "./globals.css" 5 | 6 | const inter = Inter({ subsets: ["latin"] }) 7 | 8 | export const metadata = { 9 | title: "Descope Custom Screens", 10 | description: "Custom authentication screens with Descope", 11 | generator: 'v0.dev' 12 | } 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode 18 | }) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | 31 | import './globals.css' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Descope Bring Your Own Screen](readme-banner.png) 2 | 3 | # Descope's Bring Your Own Screen Sample App 4 | 5 | Welcome to Descope's Bring Your Own Screen Sample App, a demonstration of how to integrate Descope flows with custom screens within a Next.JS application. 6 | 7 | ## Descope Bring Your Own Screen 8 | 9 | This project demonstrates Descope's "Bring Your Own Screen" feature, which allows developers to: 10 | - Customize the entire authentication UI while leveraging Descope flows and session management 11 | - Maintain full control over the user experience and design 12 | - Integrate authentication flows seamlessly into existing applications 13 | - Use pre-built components or create custom ones 14 | - Handle authentication states and transitions with ease 15 | 16 | The implementation includes: 17 | - A "Welcome Screen" that prompts the user for their email address. 18 | - A "Verify OTP" screen that prompts the user for an OTP sent to their email. 19 | - A "User Information" screen that prompts new user's to enter their full name. -------------------------------------------------------------------------------- /public/placeholder.jpg: -------------------------------------------------------------------------------- 1 | ����JFIFHH���ExifMM*JR(�iZHH�����8Photoshop 3.08BIM8BIM%��ُ�� ���B~���� 2 | ���s!1"AQ2aq#� �B�R3�$b0�r�C�4��S@%c5�s�PD���&T6d�t�`҄�p�'E7e�Uu��Å��Fv��GVf� 3 | ()*89:HIJWXYZghijwxyz����������������������������������������������������������� 4 | ����! 1A0"2Q@3#aBqR4�P$��C�b5S��%`�D�r��c6p&ET�'�� 5 | ()*789:FGHIJUVWXYZdefghijstuvwxyz�����������������������������������������������������������������������������C  6 |  7 | 8 | ")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHE��C !!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE�� �k����?��?��?��3 !1AQaq��������� 0@P`p���������?!��� ��3 !1AQa q𑁡�����0@P`p���������?���?���?��� -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | let userConfig = undefined 2 | try { 3 | // try to import ESM first 4 | userConfig = await import('./v0-user-next.config.mjs') 5 | } catch (e) { 6 | try { 7 | // fallback to CJS import 8 | userConfig = await import("./v0-user-next.config"); 9 | } catch (innerError) { 10 | // ignore error 11 | } 12 | } 13 | 14 | /** @type {import('next').NextConfig} */ 15 | const nextConfig = { 16 | eslint: { 17 | ignoreDuringBuilds: true, 18 | }, 19 | typescript: { 20 | ignoreBuildErrors: true, 21 | }, 22 | images: { 23 | unoptimized: true, 24 | }, 25 | experimental: { 26 | webpackBuildWorker: true, 27 | parallelServerBuildTraces: true, 28 | parallelServerCompiles: true, 29 | }, 30 | } 31 | 32 | if (userConfig) { 33 | // ESM imports will have a "default" property 34 | const config = userConfig.default || userConfig 35 | 36 | for (const key in config) { 37 | if ( 38 | typeof nextConfig[key] === 'object' && 39 | !Array.isArray(nextConfig[key]) 40 | ) { 41 | nextConfig[key] = { 42 | ...nextConfig[key], 43 | ...config[key], 44 | } 45 | } else { 46 | nextConfig[key] = config[key] 47 | } 48 | } 49 | } 50 | 51 | export default nextConfig 52 | -------------------------------------------------------------------------------- /public/placeholder-user.jpg: -------------------------------------------------------------------------------- 1 | ����JFIF��C 2 |   3 |  4 | $ &%# #"(-90(*6+"#2D26;=@@@&0FKE>J9?@=��C  =)#)==================================================���������� ـ|�r4�-�̈"x�'�0�Í��8�H�N�q�����Q�������V�`=�($q"_�� 5 | �S8�P��0VFbP��! 6 | Io40��[?p#�|� @!.E�3��4pBq �Z s���C  AQR�!1@Ua��02Tq���56cps�� "#$PS�����?��R���,�� ��� 7 | �n�k��n8rZ�����9Vv��V��ms$9zWʏh�-+@Z�2�PGE������EY9��i�Ͻ�S ��O��Ȕ��_I��W髵�}�����B�ՎT��>%r �[e/,W�D}��D�>b�e>�v�Z�p&�*VS��V�sV�c�:��~K�������C��:��k�'An|ʶ�}\��C� �f����a�;�h��J����q!i=���"�q�NF�IZ�`�wĝ5hAj� 8 | �RXl䎉�lk���@I�%l��Ն���FDY-����Eq�i����O�I�_�2b�lNj�Yu��k���AO����٣��ܭ�n��cam�jN�j���VL�}� ;��oކ6��շs��,���ք���l�i����l{I�O��(!%J $ ���n�-@G����n��ܮi!�괁G�:�^��n�g3l�F%���9]�Pq��)�:��� @�*ɍmׅ�VLY'�s+�z ���V�m�J9��S�_���#��;�����NJ!5�#�q\�M@��@�]yz�����A;e�k��@�s�^���G����\�5F��(��S��Ly���c�i8�����o�8T��i�N��7D����t-�p�3`r�q r�;|�.��bTG��[i H��͚-� 9 | ��Oj�H����M�ؒFE�{�3X�n���e� �R3/�~����� 10 | ��a����!�j&@^r�����Y�������l�Z? �7땵��)ki�w��\.�u�����X��\.�u�����X��\.�u�����X��\.�u��p�M����o(N��3�Vg�����Z�s��%�\�]q}d�k\_Y5���MG�����Q��q}d�k\_Y5���MG�����Q��q}d�kV|5���8���//�������?�����?�� -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /components/AuthSuccess.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { useRouter } from "next/navigation" 5 | import "./authSuccess.css" 6 | 7 | interface AuthSuccessProps { 8 | userName?: string 9 | onContinue: () => void 10 | } 11 | 12 | export default function AuthSuccess({ userName, onContinue }: AuthSuccessProps) { 13 | const [animationComplete, setAnimationComplete] = useState(false) 14 | 15 | // Handle success animation completion 16 | useEffect(() => { 17 | const animationTimer = setTimeout(() => { 18 | setAnimationComplete(true) 19 | }, 1500) 20 | 21 | return () => clearTimeout(animationTimer) 22 | }, []) 23 | 24 | const displayName = userName || "there" 25 | 26 | return ( 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |

Authentication Successful!

46 |

Welcome back, {displayName}!

47 |

48 | You are now authenticated. You can now close this tab. 49 |

50 | 51 |
52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { useDescope, useSession } from '@descope/nextjs-sdk/client'; 5 | import { useRouter } from "next/navigation" 6 | import DashboardUI from "@/components/DashboardUI" 7 | 8 | export default function Dashboard() { 9 | const router = useRouter() 10 | const { isAuthenticated, isSessionLoading, sessionToken } = useSession() 11 | const sdk = useDescope() 12 | const [userData, setUserData] = useState<{ name?: string; email?: string; picture?: string }>({ name: "User", email: "", picture: undefined }) 13 | 14 | useEffect(() => { 15 | if (!isAuthenticated && !isSessionLoading) { 16 | router.push("/") 17 | return 18 | } 19 | }, [isAuthenticated, isSessionLoading, router, sessionToken]) 20 | 21 | const handleLogout = async () => { 22 | try { 23 | await sdk.logout() 24 | router.push("/") 25 | } catch (error) { 26 | console.error("Logout failed", error) 27 | } 28 | } 29 | 30 | if (isSessionLoading) { 31 | return 32 | } 33 | 34 | return 35 | } 36 | 37 | function LoadingScreen() { 38 | return ( 39 |
40 |
41 |

Loading your dashboard...

42 | 43 | 71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-v0-project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.9.1", 13 | "@radix-ui/react-accordion": "^1.2.2", 14 | "@radix-ui/react-alert-dialog": "^1.1.4", 15 | "@radix-ui/react-aspect-ratio": "^1.1.1", 16 | "@radix-ui/react-avatar": "^1.1.2", 17 | "@radix-ui/react-checkbox": "^1.1.3", 18 | "@radix-ui/react-collapsible": "^1.1.2", 19 | "@radix-ui/react-context-menu": "^2.2.4", 20 | "@radix-ui/react-dialog": "^1.1.4", 21 | "@radix-ui/react-dropdown-menu": "^2.1.4", 22 | "@radix-ui/react-hover-card": "^1.1.4", 23 | "@radix-ui/react-label": "^2.1.1", 24 | "@radix-ui/react-menubar": "^1.1.4", 25 | "@radix-ui/react-navigation-menu": "^1.2.3", 26 | "@radix-ui/react-popover": "^1.1.4", 27 | "@radix-ui/react-progress": "^1.1.1", 28 | "@radix-ui/react-radio-group": "^1.2.2", 29 | "@radix-ui/react-scroll-area": "^1.2.2", 30 | "@radix-ui/react-select": "^2.1.4", 31 | "@radix-ui/react-separator": "^1.1.1", 32 | "@radix-ui/react-slider": "^1.2.2", 33 | "@radix-ui/react-slot": "^1.1.1", 34 | "@radix-ui/react-switch": "^1.1.2", 35 | "@radix-ui/react-tabs": "^1.1.2", 36 | "@radix-ui/react-toast": "^1.2.4", 37 | "@radix-ui/react-toggle": "^1.1.1", 38 | "@radix-ui/react-toggle-group": "^1.1.1", 39 | "@radix-ui/react-tooltip": "^1.1.6", 40 | "autoprefixer": "^10.4.20", 41 | "class-variance-authority": "^0.7.1", 42 | "clsx": "^2.1.1", 43 | "cmdk": "1.0.4", 44 | "date-fns": "^3", 45 | "embla-carousel-react": "8.5.1", 46 | "input-otp": "1.4.1", 47 | "lucide-react": "^0.454.0", 48 | "next": "15.4.10", 49 | "next-themes": "^0.4.4", 50 | "react": "^19", 51 | "react-day-picker": "9.11.3", 52 | "react-dom": "^19", 53 | "react-hook-form": "^7.54.1", 54 | "react-resizable-panels": "^2.1.7", 55 | "recharts": "2.15.0", 56 | "sonner": "^1.7.1", 57 | "tailwind-merge": "^2.5.5", 58 | "tailwindcss-animate": "^1.0.7", 59 | "vaul": "1.1.2", 60 | "zod": "^3.24.1", 61 | "@descope/nextjs-sdk": "latest" 62 | }, 63 | "devDependencies": { 64 | "@types/node": "^22", 65 | "@types/react": "^19", 66 | "@types/react-dom": "^19", 67 | "postcss": "^8", 68 | "tailwindcss": "^3.4.17", 69 | "typescript": "^5" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 0 0% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | --primary: 0 0% 9%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --accent: 0 0% 96.1%; 30 | --accent-foreground: 0 0% 9%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | --chart-1: 12 76% 61%; 37 | --chart-2: 173 58% 39%; 38 | --chart-3: 197 37% 24%; 39 | --chart-4: 43 74% 66%; 40 | --chart-5: 27 87% 67%; 41 | --radius: 0.5rem; 42 | --sidebar-background: 0 0% 98%; 43 | --sidebar-foreground: 240 5.3% 26.1%; 44 | --sidebar-primary: 240 5.9% 10%; 45 | --sidebar-primary-foreground: 0 0% 98%; 46 | --sidebar-accent: 240 4.8% 95.9%; 47 | --sidebar-accent-foreground: 240 5.9% 10%; 48 | --sidebar-border: 220 13% 91%; 49 | --sidebar-ring: 217.2 91.2% 59.8%; 50 | } 51 | .dark { 52 | --background: 0 0% 3.9%; 53 | --foreground: 0 0% 98%; 54 | --card: 0 0% 3.9%; 55 | --card-foreground: 0 0% 98%; 56 | --popover: 0 0% 3.9%; 57 | --popover-foreground: 0 0% 98%; 58 | --primary: 0 0% 98%; 59 | --primary-foreground: 0 0% 9%; 60 | --secondary: 0 0% 14.9%; 61 | --secondary-foreground: 0 0% 98%; 62 | --muted: 0 0% 14.9%; 63 | --muted-foreground: 0 0% 63.9%; 64 | --accent: 0 0% 14.9%; 65 | --accent-foreground: 0 0% 98%; 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 0 0% 98%; 68 | --border: 0 0% 14.9%; 69 | --input: 0 0% 14.9%; 70 | --ring: 0 0% 83.1%; 71 | --chart-1: 220 70% 50%; 72 | --chart-2: 160 60% 45%; 73 | --chart-3: 30 80% 55%; 74 | --chart-4: 280 65% 60%; 75 | --chart-5: 340 75% 55%; 76 | --sidebar-background: 240 5.9% 10%; 77 | --sidebar-foreground: 240 4.8% 95.9%; 78 | --sidebar-primary: 224.3 76.3% 48%; 79 | --sidebar-primary-foreground: 0 0% 100%; 80 | --sidebar-accent: 240 3.7% 15.9%; 81 | --sidebar-accent-foreground: 240 4.8% 95.9%; 82 | --sidebar-border: 240 3.7% 15.9%; 83 | --sidebar-ring: 217.2 91.2% 59.8%; 84 | } 85 | } 86 | 87 | @layer base { 88 | * { 89 | @apply border-border; 90 | } 91 | body { 92 | @apply bg-background text-foreground; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "*.{js,ts,jsx,tsx,mdx}" 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | }, 54 | sidebar: { 55 | DEFAULT: 'hsl(var(--sidebar-background))', 56 | foreground: 'hsl(var(--sidebar-foreground))', 57 | primary: 'hsl(var(--sidebar-primary))', 58 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 59 | accent: 'hsl(var(--sidebar-accent))', 60 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 61 | border: 'hsl(var(--sidebar-border))', 62 | ring: 'hsl(var(--sidebar-ring))' 63 | } 64 | }, 65 | borderRadius: { 66 | lg: 'var(--radius)', 67 | md: 'calc(var(--radius) - 2px)', 68 | sm: 'calc(var(--radius) - 4px)' 69 | }, 70 | keyframes: { 71 | 'accordion-down': { 72 | from: { 73 | height: '0' 74 | }, 75 | to: { 76 | height: 'var(--radix-accordion-content-height)' 77 | } 78 | }, 79 | 'accordion-up': { 80 | from: { 81 | height: 'var(--radix-accordion-content-height)' 82 | }, 83 | to: { 84 | height: '0' 85 | } 86 | } 87 | }, 88 | animation: { 89 | 'accordion-down': 'accordion-down 0.2s ease-out', 90 | 'accordion-up': 'accordion-up 0.2s ease-out' 91 | } 92 | } 93 | }, 94 | plugins: [require("tailwindcss-animate")], 95 | }; 96 | export default config; 97 | -------------------------------------------------------------------------------- /components/DashboardUI.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { useUser } from "@descope/nextjs-sdk/client" 5 | import "./dashboardUI.css" 6 | 7 | interface DashboardUIProps { 8 | onLogout: () => void; 9 | } 10 | 11 | export default function DashboardUI({ onLogout }: DashboardUIProps) { 12 | const { user} = useUser() 13 | const [currentTime, setCurrentTime] = useState(new Date()) 14 | 15 | // Update time every minute 16 | useEffect(() => { 17 | const timer = setInterval(() => { 18 | setCurrentTime(new Date()) 19 | }, 60000) 20 | 21 | return () => clearInterval(timer) 22 | }, []) 23 | 24 | // Format date for greeting 25 | const getGreeting = () => { 26 | const hour = currentTime.getHours() 27 | if (hour < 12) return "Good morning" 28 | if (hour < 18) return "Good afternoon" 29 | return "Good evening" 30 | } 31 | 32 | const formatDate = () => { 33 | return currentTime.toLocaleDateString("en-US", { 34 | weekday: "long", 35 | month: "long", 36 | day: "numeric", 37 | }) 38 | } 39 | 40 | const userData = { 41 | name: user?.name, 42 | email: user?.email || "", 43 | picture: user?.picture|| "", 44 | } 45 | 46 | return ( 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 | 60 | 61 | 62 |
63 | Dashboard 64 |
65 | 66 |
67 | 73 |
74 |
75 | 76 |
77 |
78 |
79 |

80 | {getGreeting()}, {userData?.name?.split(" ")[0] || "User"}! 81 |

82 |

{formatDate()}

83 |
84 |
85 |
86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /public/placeholder-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/AuthFlow.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { Descope } from "@descope/nextjs-sdk" 5 | import { useSession } from "@descope/nextjs-sdk/client" 6 | import { useRouter } from "next/navigation" 7 | import EmailInput from "./EmailInput" 8 | import OtpVerification from "./OtpVerification" 9 | import UserNamePrompt from "./UserNamePrompt" 10 | 11 | const emailScreenName = "Welcome Screen" 12 | const verifyScreenName = "Verify OTP" 13 | const nameScreenName = "User Information" 14 | 15 | interface FormState { 16 | email?: string; 17 | provider?: string; 18 | code?: string; 19 | } 20 | 21 | export default function AuthFlow() { 22 | const router = useRouter() 23 | const { isAuthenticated } = useSession() 24 | const [state, setState] = useState<{ error: { text?: string }, screenName?: string, next?: (stepId: string, data?: any) => Promise }>({ error: {} }) 25 | const [form, setForm] = useState({}) 26 | 27 | // Check if already authenticated 28 | useEffect(() => { 29 | if (isAuthenticated) { 30 | router.push("/dashboard") 31 | } 32 | }, [isAuthenticated, router]) 33 | 34 | return ( 35 | { 38 | console.log("STATE", screenName, state) 39 | setState((prevState) => ({ ...prevState, ...state, next, screenName })) 40 | 41 | return screenName === emailScreenName || screenName === verifyScreenName || screenName === nameScreenName 42 | }} 43 | onSuccess={() => { 44 | console.log("success") 45 | setState((prevState) => ({ ...prevState })) 46 | router.push("/dashboard") 47 | }} 48 | > 49 | {state?.screenName === emailScreenName && 50 | { 53 | if (state.next) { 54 | await state.next('sign-up-or-in', form) 55 | } 56 | }} 57 | errorText={state?.error?.text} 58 | onChange={() => { 59 | setState(prevState => ({ ...prevState })) 60 | }} 61 | />} 62 | {state?.screenName === verifyScreenName && 63 | { 66 | setForm(prev => ({ ...prev, ...data })) 67 | }} 68 | onSubmit={async (data) => { 69 | if (state.next) { 70 | await state.next('submit-otp', { code: data.form.code }) 71 | } 72 | }} 73 | onResendClick={async () => { 74 | if (state.next) { 75 | await state.next('resend', form) 76 | } 77 | }} 78 | onBackClick={() => { 79 | if (state.next) { 80 | state.next('back', {}) 81 | } 82 | }} 83 | errorText={state?.error?.text} 84 | onChange={() => { 85 | setState(prevState => ({ ...prevState })) 86 | }} 87 | state={state} 88 | />} 89 | 90 | {state?.screenName === nameScreenName && 91 | { 95 | if (state.next) { 96 | await state.next('submit-name', form) 97 | } 98 | }} 99 | errorText={state?.error?.text} 100 | />} 101 | 102 | 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /components/EmailInput.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useState, useEffect } from "react" 3 | import "./emailInput.css" 4 | 5 | interface EmailInputProps { 6 | onFormUpdate: (data: Record) => void; 7 | onClick: () => void; 8 | onChange: () => void; 9 | errorText?: string; 10 | } 11 | 12 | export default function EmailInput({ onFormUpdate, onClick, onChange, errorText }: EmailInputProps) { 13 | const [email, setEmail] = useState("") 14 | const [focused, setFocused] = useState(false) 15 | const [valid, setValid] = useState(true) 16 | const [animating, setAnimating] = useState(false) 17 | 18 | // Email validation 19 | useEffect(() => { 20 | if (email) { 21 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 22 | setValid(emailRegex.test(email)) 23 | } else { 24 | setValid(true) 25 | } 26 | }, [email]) 27 | 28 | const handleInputChange = (e: React.ChangeEvent) => { 29 | setEmail(e.target.value) 30 | onFormUpdate({ [e.target.name]: e.target.value }) 31 | onChange() 32 | } 33 | 34 | const handleSubmit = () => { 35 | if (email && valid) { 36 | setAnimating(true) 37 | setTimeout(() => { 38 | onClick() 39 | }, 600) 40 | } 41 | } 42 | 43 | return ( 44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 |
53 |
54 |

Welcome Back

55 |

Enter your email to continue

56 |
57 | 58 |
59 |
60 | setFocused(true)} 66 | onBlur={() => setFocused(false)} 67 | required 68 | /> 69 | Email Address 70 | 71 | {email && ( 72 | 73 | {valid ? ( 74 | 75 | 76 | 77 | ) : ( 78 | 79 | 80 | 81 | )} 82 | 83 | )} 84 |
85 | 86 | {errorText &&

{errorText}

} 87 | {!valid && email &&

Please enter a valid email address

} 88 |
89 | 90 | 104 |
105 | 106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /src/EmailInput.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useState, useEffect } from "react" 3 | import "./emailInput.css" 4 | 5 | export default ({ onFormUpdate, onClick, onChange, errorText }) => { 6 | const [email, setEmail] = useState("") 7 | const [focused, setFocused] = useState(false) 8 | const [valid, setValid] = useState(true) 9 | const [animating, setAnimating] = useState(false) 10 | 11 | // Email validation 12 | useEffect(() => { 13 | if (email) { 14 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 15 | setValid(emailRegex.test(email)) 16 | } else { 17 | setValid(true) 18 | } 19 | }, [email]) 20 | 21 | const handleInputChange = (e) => { 22 | setEmail(e.target.value) 23 | onFormUpdate({ [e.target.name]: e.target.value }) 24 | } 25 | 26 | const handleSubmit = () => { 27 | if (email && valid) { 28 | setAnimating(true) 29 | setTimeout(() => { 30 | onClick() 31 | }, 600) 32 | } 33 | } 34 | 35 | return ( 36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 |
46 |

Welcome Back

47 |

Enter your email to continue

48 |
49 | 50 |
51 |
52 | setFocused(true)} 58 | onBlur={() => setFocused(false)} 59 | required 60 | /> 61 | Email Address 62 | 63 | {email && ( 64 | 65 | {valid ? ( 66 | 67 | 68 | 69 | ) : ( 70 | 71 | 72 | 73 | )} 74 | 75 | )} 76 |
77 | 78 | {errorText &&

{errorText}

} 79 | {!valid && email &&

Please enter a valid email address

} 80 |
81 | 82 | 96 | 97 |
98 | 99 | 100 | or 101 | 102 | 103 | 104 |
105 | 111 |
112 |
113 |
114 | 115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /components/UserNamePrompt.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState, useEffect } from "react" 6 | import "./userNamePrompt.css" 7 | 8 | interface UserNamePromptProps { 9 | email?: string 10 | onSubmit: (name: string) => void 11 | errorText?: string 12 | onFormUpdate?: (data: Record) => void 13 | onChange?: () => void 14 | state?: { error?: { text?: string, code?: string }, screenName?: string, next?: (stepId: string, data?: any) => Promise } 15 | } 16 | 17 | export default function UserNamePrompt({ email, onSubmit, errorText, state, onFormUpdate, onChange }: UserNamePromptProps) { 18 | const [name, setName] = useState("") 19 | const [focused, setFocused] = useState(false) 20 | const [isSubmitting, setIsSubmitting] = useState(false) 21 | const [valid, setValid] = useState(true) 22 | 23 | // Name validation 24 | useEffect(() => { 25 | if (name) { 26 | setValid(name.trim().length >= 2) 27 | } else { 28 | setValid(true) 29 | } 30 | }, [name]) 31 | 32 | const handleInputChange = (e: React.ChangeEvent) => { 33 | const newName = e.target.value 34 | setName(newName) 35 | if (onFormUpdate) { 36 | onFormUpdate({ fullName: newName }) 37 | } 38 | if (onChange) { 39 | onChange() 40 | } 41 | } 42 | 43 | const handleSubmit = () => { 44 | if (name && valid) { 45 | setIsSubmitting(true) 46 | 47 | // Store the name in localStorage for persistence 48 | if (typeof window !== "undefined") { 49 | localStorage.setItem("user_name", name.trim()) 50 | } 51 | 52 | setTimeout(() => { 53 | onSubmit(name.trim()) 54 | }, 600) 55 | } 56 | } 57 | 58 | return ( 59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 |
68 |
69 |

Almost Done!

70 |

Please tell us your name to complete your profile

71 | {email &&
{email}
} 72 |
73 | 74 |
75 |
76 | setFocused(true)} 82 | onBlur={() => setFocused(false)} 83 | required 84 | /> 85 | Full Name 86 | 87 | {name && ( 88 | 89 | {valid ? ( 90 | 91 | 92 | 93 | ) : ( 94 | 95 | 96 | 97 | )} 98 | 99 | )} 100 |
101 | 102 | {errorText &&

{errorText}

} 103 | {!valid && name &&

Please enter a valid name (at least 2 characters)

} 104 |
105 | 106 | 120 |
121 | 122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /components/OtpVerification.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState, useEffect, useRef } from "react" 6 | import "./otpVerification.css" 7 | 8 | interface OtpVerificationProps { 9 | email: string 10 | onSubmit: (data: { form: { code: string }, sentTo: { maskedEmail: string } }) => void 11 | onResendClick: () => void 12 | onBackClick: () => void 13 | errorText?: string 14 | onFormUpdate?: (data: Record) => void 15 | onChange?: () => void 16 | state?: { error?: { text?: string, code?: string }, screenName?: string, next?: (stepId: string, data?: any) => Promise } 17 | } 18 | 19 | export default function OtpVerification({ 20 | email, 21 | onSubmit, 22 | onResendClick, 23 | onBackClick, 24 | errorText, 25 | onFormUpdate, 26 | onChange, 27 | state, 28 | }: OtpVerificationProps) { 29 | const [otp, setOtp] = useState(Array(6).fill("")) 30 | const [isSubmitting, setIsSubmitting] = useState(false) 31 | const [countdown, setCountdown] = useState(60) 32 | const [canResend, setCanResend] = useState(false) 33 | const [isResending, setIsResending] = useState(false) 34 | 35 | const inputRefs = useRef<(HTMLInputElement | null)[]>([]) 36 | 37 | // Reset OTP input when error is E061102 38 | useEffect(() => { 39 | if (state?.error?.code === "E061102") { 40 | setOtp(Array(6).fill("")) 41 | setIsSubmitting(false) 42 | } 43 | }, [state?.error?.code]) 44 | 45 | // Countdown timer for resend button 46 | useEffect(() => { 47 | if (countdown > 0 && !canResend) { 48 | const timer = setTimeout(() => { 49 | setCountdown(countdown - 1) 50 | }, 1000) 51 | return () => clearTimeout(timer) 52 | } else if (countdown === 0 && !canResend) { 53 | setCanResend(true) 54 | } 55 | }, [countdown, canResend]) 56 | 57 | // Handle OTP input change 58 | const handleChange = (index: number, value: string) => { 59 | // Only allow numbers 60 | if (!/^\d*$/.test(value)) return 61 | 62 | const newOtp = [...otp] 63 | newOtp[index] = value.slice(0, 1) 64 | setOtp(newOtp) 65 | 66 | // Update form state 67 | if (onFormUpdate) { 68 | onFormUpdate({ code: newOtp.join('') }) 69 | } 70 | if (onChange) { 71 | onChange() 72 | } 73 | 74 | // Auto-focus next input 75 | if (value && index < 5) { 76 | inputRefs.current[index + 1]?.focus() 77 | } 78 | } 79 | 80 | // Handle key press for backspace 81 | const handleKeyDown = (index: number, e: React.KeyboardEvent) => { 82 | if (e.key === "Backspace" && !otp[index] && index > 0) { 83 | // Focus previous input when backspace is pressed on empty input 84 | inputRefs.current[index - 1]?.focus() 85 | } 86 | } 87 | 88 | // Handle paste functionality 89 | const handlePaste = (e: React.ClipboardEvent) => { 90 | e.preventDefault() 91 | const pastedData = e.clipboardData.getData("text/plain").trim() 92 | 93 | // Check if pasted content is a valid OTP (numbers only) 94 | if (!/^\d+$/.test(pastedData)) return 95 | 96 | // Fill the OTP fields with pasted data 97 | const newOtp = [...otp] 98 | for (let i = 0; i < Math.min(pastedData.length, 6); i++) { 99 | newOtp[i] = pastedData[i] 100 | } 101 | setOtp(newOtp) 102 | 103 | // Focus the next empty field or the last field 104 | const nextEmptyIndex = newOtp.findIndex((val) => !val) 105 | if (nextEmptyIndex !== -1 && nextEmptyIndex < 6) { 106 | inputRefs.current[nextEmptyIndex]?.focus() 107 | } else { 108 | inputRefs.current[5]?.focus() 109 | } 110 | } 111 | 112 | // Handle form submission 113 | const handleSubmit = () => { 114 | const otpValue = otp.join("") 115 | if (otpValue.length === 6) { 116 | setIsSubmitting(true) 117 | onSubmit({ 118 | form: { 119 | code: otpValue 120 | }, 121 | sentTo: { 122 | maskedEmail: email 123 | } 124 | }) 125 | 126 | // Reset submitting state after a delay (in case of error) 127 | setTimeout(() => { 128 | setIsSubmitting(false) 129 | }, 2000) 130 | } 131 | } 132 | 133 | // Handle resend click 134 | const handleResend = () => { 135 | if (canResend) { 136 | setIsResending(true) 137 | onResendClick() 138 | 139 | // Reset the countdown 140 | setTimeout(() => { 141 | setIsResending(false) 142 | setCanResend(false) 143 | setCountdown(60) 144 | }, 1000) 145 | } 146 | } 147 | 148 | return ( 149 |
150 |
151 |
152 |
153 |
154 | 155 | 156 | 157 |
158 |
159 |

Verification Code

160 |

Enter the 6-digit code sent to

161 |
{email}
162 |
163 | 164 |
165 | {otp.map((digit, index) => ( 166 | handleChange(index, e.target.value)} 172 | onKeyDown={(e) => handleKeyDown(index, e)} 173 | onPaste={index === 0 ? handlePaste : undefined} 174 | ref={(el) => { inputRefs.current[index] = el }} 175 | className="otp-input" 176 | autoFocus={index === 0} 177 | /> 178 | ))} 179 |
180 | 181 | {errorText &&

{errorText}

} 182 | 183 | 191 | 192 |
193 | 200 | 201 | 204 |
205 |
206 | 207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | ) 215 | } 216 | -------------------------------------------------------------------------------- /components/authSuccess.css: -------------------------------------------------------------------------------- 1 | .auth-success-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 10 | "Helvetica Neue", sans-serif; 11 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 12 | } 13 | 14 | .auth-success-card { 15 | width: 100%; 16 | max-width: 420px; 17 | background: white; 18 | border-radius: 16px; 19 | padding: 40px 30px; 20 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); 21 | position: relative; 22 | z-index: 10; 23 | overflow: hidden; 24 | animation: cardAppear 0.6s ease-out; 25 | text-align: center; 26 | } 27 | 28 | @keyframes cardAppear { 29 | 0% { 30 | opacity: 0; 31 | transform: translateY(20px); 32 | } 33 | 100% { 34 | opacity: 1; 35 | transform: translateY(0); 36 | } 37 | } 38 | 39 | .success-animation { 40 | position: relative; 41 | height: 120px; 42 | margin: 0 auto 30px; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .checkmark-circle { 49 | position: relative; 50 | width: 100px; 51 | height: 100px; 52 | transform: scale(0); 53 | animation: scaleIn 0.5s cubic-bezier(0.22, 0.68, 0, 1.71) forwards; 54 | animation-delay: 0.3s; 55 | } 56 | 57 | @keyframes scaleIn { 58 | 0% { 59 | transform: scale(0); 60 | } 61 | 100% { 62 | transform: scale(1); 63 | } 64 | } 65 | 66 | .checkmark-circle-bg { 67 | position: absolute; 68 | width: 100%; 69 | height: 100%; 70 | border-radius: 50%; 71 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 72 | box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); 73 | } 74 | 75 | .checkmark-check { 76 | position: absolute; 77 | top: 50%; 78 | left: 50%; 79 | width: 50px; 80 | height: 25px; 81 | border-bottom: 8px solid white; 82 | border-left: 8px solid white; 83 | transform: translate(-50%, -60%) rotate(-45deg) scale(0); 84 | animation: checkmark 0.5s cubic-bezier(0.22, 0.68, 0, 1.71) forwards; 85 | animation-delay: 0.8s; 86 | transform-origin: center; 87 | } 88 | 89 | @keyframes checkmark { 90 | 0% { 91 | transform: translate(-50%, -60%) rotate(-45deg) scale(0); 92 | } 93 | 100% { 94 | transform: translate(-50%, -60%) rotate(-45deg) scale(1); 95 | } 96 | } 97 | 98 | .success-sparkles { 99 | position: absolute; 100 | width: 200px; 101 | height: 200px; 102 | top: 50%; 103 | left: 50%; 104 | transform: translate(-50%, -50%); 105 | opacity: 0; 106 | animation: fadeIn 0.3s ease forwards; 107 | animation-delay: 1s; 108 | } 109 | 110 | .success-sparkle { 111 | position: absolute; 112 | width: 15px; 113 | height: 15px; 114 | border-radius: 50%; 115 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 116 | opacity: 0; 117 | } 118 | 119 | .success-animation.complete .success-sparkle { 120 | animation: sparkleOut 1s ease-out forwards; 121 | } 122 | 123 | .sparkle-1 { 124 | top: 0; 125 | left: 50%; 126 | transform: translateX(-50%); 127 | animation-delay: 0.1s; 128 | } 129 | 130 | .sparkle-2 { 131 | top: 50%; 132 | right: 0; 133 | transform: translateY(-50%); 134 | animation-delay: 0.2s; 135 | } 136 | 137 | .sparkle-3 { 138 | bottom: 0; 139 | left: 50%; 140 | transform: translateX(-50%); 141 | animation-delay: 0.3s; 142 | } 143 | 144 | .sparkle-4 { 145 | top: 50%; 146 | left: 0; 147 | transform: translateY(-50%); 148 | animation-delay: 0.4s; 149 | } 150 | 151 | .sparkle-5 { 152 | top: 25%; 153 | left: 25%; 154 | animation-delay: 0.5s; 155 | } 156 | 157 | .sparkle-6 { 158 | bottom: 25%; 159 | right: 25%; 160 | animation-delay: 0.6s; 161 | } 162 | 163 | @keyframes sparkleOut { 164 | 0% { 165 | opacity: 0; 166 | transform: scale(0) translateX(0) translateY(0); 167 | } 168 | 20% { 169 | opacity: 1; 170 | transform: scale(1) translateX(0) translateY(0); 171 | } 172 | 100% { 173 | opacity: 0; 174 | transform: scale(0) translateX(var(--tx, 50px)) translateY(var(--ty, 50px)); 175 | } 176 | } 177 | 178 | .sparkle-1 { 179 | --tx: 0; 180 | --ty: -80px; 181 | } 182 | 183 | .sparkle-2 { 184 | --tx: 80px; 185 | --ty: 0; 186 | } 187 | 188 | .sparkle-3 { 189 | --tx: 0; 190 | --ty: 80px; 191 | } 192 | 193 | .sparkle-4 { 194 | --tx: -80px; 195 | --ty: 0; 196 | } 197 | 198 | .sparkle-5 { 199 | --tx: -60px; 200 | --ty: -60px; 201 | } 202 | 203 | .sparkle-6 { 204 | --tx: 60px; 205 | --ty: 60px; 206 | } 207 | 208 | .success-content { 209 | opacity: 0; 210 | transform: translateY(20px); 211 | transition: opacity 0.5s ease, transform 0.5s ease; 212 | } 213 | 214 | .success-content.visible { 215 | opacity: 1; 216 | transform: translateY(0); 217 | } 218 | 219 | .success-content h2 { 220 | font-size: 24px; 221 | font-weight: 700; 222 | color: #1f2937; 223 | margin: 0 0 15px 0; 224 | } 225 | 226 | .welcome-message { 227 | font-size: 18px; 228 | color: #4b5563; 229 | margin: 0 0 20px 0; 230 | } 231 | 232 | .redirect-message { 233 | font-size: 16px; 234 | color: #6b7280; 235 | margin: 0; 236 | } 237 | 238 | .countdown { 239 | font-weight: 600; 240 | color: #6366f1; 241 | font-size: 18px; 242 | } 243 | 244 | .background-shapes { 245 | position: absolute; 246 | top: 0; 247 | left: 0; 248 | width: 100%; 249 | height: 100%; 250 | z-index: 1; 251 | overflow: hidden; 252 | } 253 | 254 | .shape { 255 | position: absolute; 256 | border-radius: 50%; 257 | opacity: 0.4; 258 | } 259 | 260 | .shape-1 { 261 | width: 300px; 262 | height: 300px; 263 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 264 | top: -150px; 265 | left: -150px; 266 | animation: float-bg 15s infinite alternate; 267 | } 268 | 269 | .shape-2 { 270 | width: 200px; 271 | height: 200px; 272 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 273 | bottom: -100px; 274 | right: -100px; 275 | animation: float-bg 20s infinite alternate-reverse; 276 | } 277 | 278 | .shape-3 { 279 | width: 150px; 280 | height: 150px; 281 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 282 | top: 60%; 283 | left: -75px; 284 | animation: float-bg 18s infinite alternate; 285 | } 286 | 287 | .shape-4 { 288 | width: 100px; 289 | height: 100px; 290 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 291 | top: 10%; 292 | right: -50px; 293 | animation: float-bg 12s infinite alternate-reverse; 294 | } 295 | 296 | @keyframes float-bg { 297 | 0% { 298 | transform: translate(0, 0) rotate(0deg); 299 | } 300 | 100% { 301 | transform: translate(30px, 30px) rotate(15deg); 302 | } 303 | } 304 | 305 | @keyframes fadeIn { 306 | 0% { 307 | opacity: 0; 308 | } 309 | 100% { 310 | opacity: 1; 311 | } 312 | } 313 | 314 | @media (max-width: 480px) { 315 | .auth-success-card { 316 | padding: 30px 20px; 317 | border-radius: 12px; 318 | } 319 | 320 | .success-content h2 { 321 | font-size: 22px; 322 | } 323 | 324 | .welcome-message { 325 | font-size: 16px; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /components/otpVerification.css: -------------------------------------------------------------------------------- 1 | .otp-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 10 | "Helvetica Neue", sans-serif; 11 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 12 | } 13 | 14 | .otp-card { 15 | width: 100%; 16 | max-width: 420px; 17 | background: white; 18 | border-radius: 16px; 19 | padding: 40px 30px; 20 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); 21 | position: relative; 22 | z-index: 10; 23 | overflow: hidden; 24 | animation: cardAppear 0.6s ease-out; 25 | } 26 | 27 | @keyframes cardAppear { 28 | 0% { 29 | opacity: 0; 30 | transform: translateY(20px); 31 | } 32 | 100% { 33 | opacity: 1; 34 | transform: translateY(0); 35 | } 36 | } 37 | 38 | .card-header { 39 | text-align: center; 40 | margin-bottom: 30px; 41 | } 42 | 43 | .logo-container { 44 | display: flex; 45 | justify-content: center; 46 | margin-bottom: 20px; 47 | } 48 | 49 | .logo-circle { 50 | width: 70px; 51 | height: 70px; 52 | border-radius: 50%; 53 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); 58 | animation: pulse 2s infinite; 59 | } 60 | 61 | @keyframes pulse { 62 | 0% { 63 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); 64 | } 65 | 70% { 66 | box-shadow: 0 0 0 15px rgba(99, 102, 241, 0); 67 | } 68 | 100% { 69 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); 70 | } 71 | } 72 | 73 | .logo-icon { 74 | width: 36px; 75 | height: 36px; 76 | fill: white; 77 | } 78 | 79 | .card-header h2 { 80 | font-size: 24px; 81 | font-weight: 700; 82 | color: #1f2937; 83 | margin: 0 0 8px 0; 84 | } 85 | 86 | .card-header p { 87 | font-size: 16px; 88 | color: #6b7280; 89 | margin: 0 0 8px 0; 90 | } 91 | 92 | .email-display { 93 | font-size: 16px; 94 | font-weight: 600; 95 | color: #4b5563; 96 | background-color: #f3f4f6; 97 | padding: 8px 16px; 98 | border-radius: 8px; 99 | margin: 8px auto; 100 | max-width: 90%; 101 | word-break: break-all; 102 | } 103 | 104 | .otp-input-group { 105 | display: flex; 106 | justify-content: space-between; 107 | margin: 30px 0; 108 | gap: 10px; 109 | } 110 | 111 | .otp-input { 112 | width: 50px; 113 | height: 60px; 114 | border: 2px solid #e5e7eb; 115 | border-radius: 8px; 116 | font-size: 24px; 117 | font-weight: 600; 118 | text-align: center; 119 | color: #1f2937; 120 | background-color: #f9fafb; 121 | transition: all 0.3s; 122 | outline: none; 123 | } 124 | 125 | .otp-input:focus { 126 | border-color: #6366f1; 127 | box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); 128 | background-color: white; 129 | } 130 | 131 | .otp-input::-webkit-inner-spin-button, 132 | .otp-input::-webkit-outer-spin-button { 133 | -webkit-appearance: none; 134 | margin: 0; 135 | } 136 | 137 | .error-message { 138 | color: #ef4444; 139 | font-size: 14px; 140 | text-align: center; 141 | margin: 0 0 20px 0; 142 | animation: fadeIn 0.3s ease; 143 | } 144 | 145 | @keyframes fadeIn { 146 | from { 147 | opacity: 0; 148 | transform: translateY(-10px); 149 | } 150 | to { 151 | opacity: 1; 152 | transform: translateY(0); 153 | } 154 | } 155 | 156 | .submit-button { 157 | width: 100%; 158 | height: 56px; 159 | border: none; 160 | border-radius: 8px; 161 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 162 | color: white; 163 | font-size: 16px; 164 | font-weight: 600; 165 | cursor: pointer; 166 | position: relative; 167 | overflow: hidden; 168 | transition: all 0.3s; 169 | display: flex; 170 | align-items: center; 171 | justify-content: center; 172 | box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); 173 | margin-bottom: 20px; 174 | } 175 | 176 | .submit-button:hover:not(:disabled) { 177 | transform: translateY(-2px); 178 | box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); 179 | } 180 | 181 | .submit-button:active:not(:disabled) { 182 | transform: translateY(0); 183 | box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 184 | } 185 | 186 | .submit-button.disabled { 187 | background: linear-gradient(135deg, #a5a6f6 0%, #c4b5f8 100%); 188 | cursor: not-allowed; 189 | transform: none; 190 | box-shadow: none; 191 | } 192 | 193 | .button-text { 194 | transition: all 0.3s; 195 | display: flex; 196 | align-items: center; 197 | } 198 | 199 | .submit-button.submitting .button-text { 200 | opacity: 0; 201 | } 202 | 203 | .loading-spinner { 204 | position: absolute; 205 | width: 24px; 206 | height: 24px; 207 | border: 3px solid rgba(255, 255, 255, 0.3); 208 | border-radius: 50%; 209 | border-top-color: white; 210 | animation: spin 1s linear infinite; 211 | opacity: 0; 212 | transition: opacity 0.3s; 213 | } 214 | 215 | .submit-button.submitting .loading-spinner { 216 | opacity: 1; 217 | } 218 | 219 | @keyframes spin { 220 | to { 221 | transform: rotate(360deg); 222 | } 223 | } 224 | 225 | .action-links { 226 | display: flex; 227 | flex-direction: column; 228 | align-items: center; 229 | gap: 15px; 230 | } 231 | 232 | .resend-link, 233 | .back-link { 234 | background: none; 235 | border: none; 236 | font-size: 14px; 237 | cursor: pointer; 238 | transition: color 0.2s; 239 | padding: 0; 240 | } 241 | 242 | .resend-link.active { 243 | color: #6366f1; 244 | font-weight: 500; 245 | } 246 | 247 | .resend-link.active:hover { 248 | text-decoration: underline; 249 | } 250 | 251 | .resend-link.disabled { 252 | color: #9ca3af; 253 | cursor: default; 254 | } 255 | 256 | .resend-link.resending { 257 | color: #9ca3af; 258 | cursor: wait; 259 | } 260 | 261 | .back-link { 262 | color: #6b7280; 263 | } 264 | 265 | .back-link:hover { 266 | color: #4b5563; 267 | text-decoration: underline; 268 | } 269 | 270 | .background-shapes { 271 | position: absolute; 272 | top: 0; 273 | left: 0; 274 | width: 100%; 275 | height: 100%; 276 | z-index: 1; 277 | overflow: hidden; 278 | } 279 | 280 | .shape { 281 | position: absolute; 282 | border-radius: 50%; 283 | opacity: 0.4; 284 | } 285 | 286 | .shape-1 { 287 | width: 300px; 288 | height: 300px; 289 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 290 | top: -150px; 291 | left: -150px; 292 | animation: float-bg 15s infinite alternate; 293 | } 294 | 295 | .shape-2 { 296 | width: 200px; 297 | height: 200px; 298 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 299 | bottom: -100px; 300 | right: -100px; 301 | animation: float-bg 20s infinite alternate-reverse; 302 | } 303 | 304 | .shape-3 { 305 | width: 150px; 306 | height: 150px; 307 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 308 | top: 60%; 309 | left: -75px; 310 | animation: float-bg 18s infinite alternate; 311 | } 312 | 313 | .shape-4 { 314 | width: 100px; 315 | height: 100px; 316 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 317 | top: 10%; 318 | right: -50px; 319 | animation: float-bg 12s infinite alternate-reverse; 320 | } 321 | 322 | @keyframes float-bg { 323 | 0% { 324 | transform: translate(0, 0) rotate(0deg); 325 | } 326 | 100% { 327 | transform: translate(30px, 30px) rotate(15deg); 328 | } 329 | } 330 | 331 | @media (max-width: 480px) { 332 | .otp-card { 333 | padding: 30px 20px; 334 | border-radius: 12px; 335 | } 336 | 337 | .logo-circle { 338 | width: 60px; 339 | height: 60px; 340 | } 341 | 342 | .logo-icon { 343 | width: 30px; 344 | height: 30px; 345 | } 346 | 347 | .card-header h2 { 348 | font-size: 22px; 349 | } 350 | 351 | .card-header p { 352 | font-size: 14px; 353 | } 354 | 355 | .otp-input { 356 | width: 40px; 357 | height: 50px; 358 | font-size: 20px; 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /components/userNamePrompt.css: -------------------------------------------------------------------------------- 1 | .name-prompt-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 10 | "Helvetica Neue", sans-serif; 11 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 12 | } 13 | 14 | .name-prompt-card { 15 | width: 100%; 16 | max-width: 420px; 17 | background: white; 18 | border-radius: 16px; 19 | padding: 40px 30px; 20 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); 21 | position: relative; 22 | z-index: 10; 23 | overflow: hidden; 24 | animation: cardAppear 0.6s ease-out; 25 | } 26 | 27 | @keyframes cardAppear { 28 | 0% { 29 | opacity: 0; 30 | transform: translateY(20px); 31 | } 32 | 100% { 33 | opacity: 1; 34 | transform: translateY(0); 35 | } 36 | } 37 | 38 | .card-header { 39 | text-align: center; 40 | margin-bottom: 30px; 41 | } 42 | 43 | .logo-container { 44 | display: flex; 45 | justify-content: center; 46 | margin-bottom: 20px; 47 | } 48 | 49 | .logo-circle { 50 | width: 70px; 51 | height: 70px; 52 | border-radius: 50%; 53 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); 58 | animation: pulse 2s infinite; 59 | } 60 | 61 | @keyframes pulse { 62 | 0% { 63 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); 64 | } 65 | 70% { 66 | box-shadow: 0 0 0 15px rgba(99, 102, 241, 0); 67 | } 68 | 100% { 69 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); 70 | } 71 | } 72 | 73 | .logo-icon { 74 | width: 36px; 75 | height: 36px; 76 | fill: white; 77 | } 78 | 79 | .card-header h2 { 80 | font-size: 24px; 81 | font-weight: 700; 82 | color: #1f2937; 83 | margin: 0 0 8px 0; 84 | } 85 | 86 | .card-header p { 87 | font-size: 16px; 88 | color: #6b7280; 89 | margin: 0 0 8px 0; 90 | } 91 | 92 | .email-display { 93 | font-size: 16px; 94 | font-weight: 600; 95 | color: #4b5563; 96 | background-color: #f3f4f6; 97 | padding: 8px 16px; 98 | border-radius: 8px; 99 | margin: 12px auto 0; 100 | max-width: 90%; 101 | word-break: break-all; 102 | } 103 | 104 | .input-group { 105 | margin-bottom: 30px; 106 | } 107 | 108 | .custom-input { 109 | position: relative; 110 | margin-bottom: 20px; 111 | } 112 | 113 | .custom-input input { 114 | width: 100%; 115 | height: 56px; 116 | padding: 20px 16px 8px; 117 | font-size: 16px; 118 | border: none; 119 | border-radius: 8px; 120 | background-color: #f3f4f6; 121 | color: #1f2937; 122 | transition: all 0.3s; 123 | outline: none; 124 | z-index: 1; 125 | position: relative; 126 | } 127 | 128 | .floating-label { 129 | position: absolute; 130 | left: 16px; 131 | top: 18px; 132 | font-size: 16px; 133 | color: #6b7280; 134 | transition: all 0.3s ease; 135 | pointer-events: none; 136 | z-index: 2; 137 | } 138 | 139 | .floating-label.active { 140 | top: 8px; 141 | font-size: 12px; 142 | color: #6366f1; 143 | } 144 | 145 | .custom-input.invalid input { 146 | background-color: #fee2e2; 147 | } 148 | 149 | .validation-icon { 150 | position: absolute; 151 | right: 16px; 152 | top: 50%; 153 | transform: translateY(-50%); 154 | width: 20px; 155 | height: 20px; 156 | z-index: 2; 157 | } 158 | 159 | .validation-icon svg { 160 | width: 100%; 161 | height: 100%; 162 | } 163 | 164 | .validation-icon.valid svg { 165 | fill: #10b981; 166 | } 167 | 168 | .validation-icon.invalid svg { 169 | fill: #ef4444; 170 | } 171 | 172 | .error-message, 173 | .validation-message { 174 | color: #ef4444; 175 | font-size: 14px; 176 | margin: 5px 0 0 0; 177 | animation: fadeIn 0.3s ease; 178 | } 179 | 180 | @keyframes fadeIn { 181 | from { 182 | opacity: 0; 183 | transform: translateY(-10px); 184 | } 185 | to { 186 | opacity: 1; 187 | transform: translateY(0); 188 | } 189 | } 190 | 191 | .submit-button { 192 | width: 100%; 193 | height: 56px; 194 | border: none; 195 | border-radius: 8px; 196 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 197 | color: white; 198 | font-size: 16px; 199 | font-weight: 600; 200 | cursor: pointer; 201 | position: relative; 202 | overflow: hidden; 203 | transition: all 0.3s; 204 | display: flex; 205 | align-items: center; 206 | justify-content: center; 207 | box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); 208 | } 209 | 210 | .submit-button:hover { 211 | transform: translateY(-2px); 212 | box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); 213 | } 214 | 215 | .submit-button:active { 216 | transform: translateY(0); 217 | box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 218 | } 219 | 220 | .submit-button.disabled { 221 | background: linear-gradient(135deg, #a5a6f6 0%, #c4b5f8 100%); 222 | cursor: not-allowed; 223 | transform: none; 224 | box-shadow: none; 225 | } 226 | 227 | .button-text { 228 | transition: all 0.3s; 229 | display: flex; 230 | align-items: center; 231 | } 232 | 233 | .button-icon { 234 | width: 20px; 235 | height: 20px; 236 | margin-left: 8px; 237 | opacity: 0; 238 | transform: translateX(-10px); 239 | transition: all 0.3s; 240 | } 241 | 242 | .button-icon svg { 243 | fill: white; 244 | width: 100%; 245 | height: 100%; 246 | } 247 | 248 | .submit-button:hover .button-icon { 249 | opacity: 1; 250 | transform: translateX(0); 251 | } 252 | 253 | .submit-button.submitting .button-text, 254 | .submit-button.submitting .button-icon { 255 | opacity: 0; 256 | } 257 | 258 | .loading-spinner { 259 | position: absolute; 260 | width: 24px; 261 | height: 24px; 262 | border: 3px solid rgba(255, 255, 255, 0.3); 263 | border-radius: 50%; 264 | border-top-color: white; 265 | animation: spin 1s linear infinite; 266 | opacity: 0; 267 | transition: opacity 0.3s; 268 | } 269 | 270 | .submit-button.submitting .loading-spinner { 271 | opacity: 1; 272 | } 273 | 274 | @keyframes spin { 275 | to { 276 | transform: rotate(360deg); 277 | } 278 | } 279 | 280 | .background-shapes { 281 | position: absolute; 282 | top: 0; 283 | left: 0; 284 | width: 100%; 285 | height: 100%; 286 | z-index: 1; 287 | overflow: hidden; 288 | } 289 | 290 | .shape { 291 | position: absolute; 292 | border-radius: 50%; 293 | opacity: 0.4; 294 | } 295 | 296 | .shape-1 { 297 | width: 300px; 298 | height: 300px; 299 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 300 | top: -150px; 301 | left: -150px; 302 | animation: float 15s infinite alternate; 303 | } 304 | 305 | .shape-2 { 306 | width: 200px; 307 | height: 200px; 308 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 309 | bottom: -100px; 310 | right: -100px; 311 | animation: float 20s infinite alternate-reverse; 312 | } 313 | 314 | .shape-3 { 315 | width: 150px; 316 | height: 150px; 317 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 318 | top: 60%; 319 | left: -75px; 320 | animation: float 18s infinite alternate; 321 | } 322 | 323 | .shape-4 { 324 | width: 100px; 325 | height: 100px; 326 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 327 | top: 10%; 328 | right: -50px; 329 | animation: float 12s infinite alternate-reverse; 330 | } 331 | 332 | @keyframes float { 333 | 0% { 334 | transform: translate(0, 0) rotate(0deg); 335 | } 336 | 100% { 337 | transform: translate(30px, 30px) rotate(15deg); 338 | } 339 | } 340 | 341 | @media (max-width: 480px) { 342 | .name-prompt-card { 343 | padding: 30px 20px; 344 | border-radius: 12px; 345 | } 346 | 347 | .logo-circle { 348 | width: 60px; 349 | height: 60px; 350 | } 351 | 352 | .logo-icon { 353 | width: 30px; 354 | height: 30px; 355 | } 356 | 357 | .card-header h2 { 358 | font-size: 22px; 359 | } 360 | 361 | .card-header p { 362 | font-size: 14px; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /components/emailInput.css: -------------------------------------------------------------------------------- 1 | .email-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 10 | "Helvetica Neue", sans-serif; 11 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 12 | } 13 | 14 | .email-card { 15 | width: 100%; 16 | max-width: 420px; 17 | background: white; 18 | border-radius: 16px; 19 | padding: 40px 30px; 20 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); 21 | position: relative; 22 | z-index: 10; 23 | overflow: hidden; 24 | animation: cardAppear 0.6s ease-out; 25 | } 26 | 27 | @keyframes cardAppear { 28 | 0% { 29 | opacity: 0; 30 | transform: translateY(20px); 31 | } 32 | 100% { 33 | opacity: 1; 34 | transform: translateY(0); 35 | } 36 | } 37 | 38 | .card-header { 39 | text-align: center; 40 | margin-bottom: 30px; 41 | } 42 | 43 | .logo-container { 44 | display: flex; 45 | justify-content: center; 46 | margin-bottom: 20px; 47 | } 48 | 49 | .logo-circle { 50 | width: 70px; 51 | height: 70px; 52 | border-radius: 50%; 53 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); 58 | animation: pulse 2s infinite; 59 | } 60 | 61 | @keyframes pulse { 62 | 0% { 63 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); 64 | } 65 | 70% { 66 | box-shadow: 0 0 0 15px rgba(99, 102, 241, 0); 67 | } 68 | 100% { 69 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); 70 | } 71 | } 72 | 73 | .logo-icon { 74 | width: 36px; 75 | height: 36px; 76 | fill: white; 77 | } 78 | 79 | .card-header h2 { 80 | font-size: 24px; 81 | font-weight: 700; 82 | color: #1f2937; 83 | margin: 0 0 8px 0; 84 | } 85 | 86 | .card-header p { 87 | font-size: 16px; 88 | color: #6b7280; 89 | margin: 0; 90 | } 91 | 92 | .input-group { 93 | margin-bottom: 30px; 94 | } 95 | 96 | .custom-input { 97 | position: relative; 98 | margin-bottom: 20px; 99 | } 100 | 101 | .custom-input input { 102 | width: 100%; 103 | height: 56px; 104 | padding: 20px 16px 8px; 105 | font-size: 16px; 106 | border: none; 107 | border-radius: 8px; 108 | background-color: #f3f4f6; 109 | color: #1f2937; 110 | transition: all 0.3s; 111 | outline: none; 112 | z-index: 1; 113 | position: relative; 114 | } 115 | 116 | .floating-label { 117 | position: absolute; 118 | left: 16px; 119 | top: 18px; 120 | font-size: 16px; 121 | color: #6b7280; 122 | transition: all 0.3s ease; 123 | pointer-events: none; 124 | z-index: 2; 125 | } 126 | 127 | .floating-label.active { 128 | top: 8px; 129 | font-size: 12px; 130 | color: #6366f1; 131 | } 132 | 133 | .custom-input.invalid input { 134 | background-color: #fee2e2; 135 | } 136 | 137 | .validation-icon { 138 | position: absolute; 139 | right: 16px; 140 | top: 50%; 141 | transform: translateY(-50%); 142 | width: 20px; 143 | height: 20px; 144 | z-index: 2; 145 | } 146 | 147 | .validation-icon svg { 148 | width: 100%; 149 | height: 100%; 150 | } 151 | 152 | .validation-icon.valid svg { 153 | fill: #10b981; 154 | } 155 | 156 | .validation-icon.invalid svg { 157 | fill: #ef4444; 158 | } 159 | 160 | .error-message, 161 | .validation-message { 162 | color: #ef4444; 163 | font-size: 14px; 164 | margin: 5px 0 0 0; 165 | animation: fadeIn 0.3s ease; 166 | } 167 | 168 | @keyframes fadeIn { 169 | from { 170 | opacity: 0; 171 | transform: translateY(-10px); 172 | } 173 | to { 174 | opacity: 1; 175 | transform: translateY(0); 176 | } 177 | } 178 | 179 | .sign-in-button { 180 | width: 100%; 181 | height: 56px; 182 | border: none; 183 | border-radius: 8px; 184 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 185 | color: white; 186 | font-size: 16px; 187 | font-weight: 600; 188 | cursor: pointer; 189 | position: relative; 190 | overflow: hidden; 191 | transition: all 0.3s; 192 | display: flex; 193 | align-items: center; 194 | justify-content: center; 195 | box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); 196 | } 197 | 198 | .sign-in-button:hover { 199 | transform: translateY(-2px); 200 | box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); 201 | } 202 | 203 | .sign-in-button:active { 204 | transform: translateY(0); 205 | box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 206 | } 207 | 208 | .sign-in-button.disabled { 209 | background: linear-gradient(135deg, #a5a6f6 0%, #c4b5f8 100%); 210 | cursor: not-allowed; 211 | transform: none; 212 | box-shadow: none; 213 | } 214 | 215 | .button-text { 216 | transition: all 0.3s; 217 | display: flex; 218 | align-items: center; 219 | } 220 | 221 | .button-icon { 222 | width: 20px; 223 | height: 20px; 224 | margin-left: 8px; 225 | opacity: 0; 226 | transform: translateX(-10px); 227 | transition: all 0.3s; 228 | } 229 | 230 | .button-icon svg { 231 | fill: white; 232 | width: 100%; 233 | height: 100%; 234 | } 235 | 236 | .sign-in-button:hover .button-icon { 237 | opacity: 1; 238 | transform: translateX(0); 239 | } 240 | 241 | .sign-in-button.animating .button-text, 242 | .sign-in-button.animating .button-icon { 243 | opacity: 0; 244 | } 245 | 246 | .loading-spinner { 247 | position: absolute; 248 | width: 24px; 249 | height: 24px; 250 | border: 3px solid rgba(255, 255, 255, 0.3); 251 | border-radius: 50%; 252 | border-top-color: white; 253 | animation: spin 1s linear infinite; 254 | opacity: 0; 255 | transition: opacity 0.3s; 256 | } 257 | 258 | .sign-in-button.animating .loading-spinner { 259 | opacity: 1; 260 | } 261 | 262 | @keyframes spin { 263 | to { 264 | transform: rotate(360deg); 265 | } 266 | } 267 | 268 | .alternative-options { 269 | margin-top: 30px; 270 | } 271 | 272 | .divider { 273 | display: flex; 274 | align-items: center; 275 | margin: 20px 0; 276 | } 277 | 278 | .divider-line { 279 | flex-grow: 1; 280 | height: 1px; 281 | background-color: #e5e7eb; 282 | } 283 | 284 | .divider-text { 285 | padding: 0 15px; 286 | color: #9ca3af; 287 | font-size: 14px; 288 | } 289 | 290 | .social-buttons { 291 | display: flex; 292 | flex-direction: column; 293 | gap: 12px; 294 | } 295 | 296 | .social-button { 297 | display: flex; 298 | align-items: center; 299 | justify-content: center; 300 | height: 48px; 301 | border-radius: 8px; 302 | border: 1px solid #e5e7eb; 303 | background-color: white; 304 | font-size: 15px; 305 | font-weight: 500; 306 | color: #4b5563; 307 | cursor: pointer; 308 | transition: all 0.2s; 309 | } 310 | 311 | .social-button:hover { 312 | background-color: #f9fafb; 313 | border-color: #d1d5db; 314 | } 315 | 316 | .social-button svg { 317 | width: 20px; 318 | height: 20px; 319 | margin-right: 10px; 320 | } 321 | 322 | .social-button.google svg { 323 | fill: #ea4335; 324 | } 325 | 326 | .background-shapes { 327 | position: absolute; 328 | top: 0; 329 | left: 0; 330 | width: 100%; 331 | height: 100%; 332 | z-index: 1; 333 | overflow: hidden; 334 | } 335 | 336 | .shape { 337 | position: absolute; 338 | border-radius: 50%; 339 | opacity: 0.4; 340 | } 341 | 342 | .shape-1 { 343 | width: 300px; 344 | height: 300px; 345 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 346 | top: -150px; 347 | left: -150px; 348 | animation: float 15s infinite alternate; 349 | } 350 | 351 | .shape-2 { 352 | width: 200px; 353 | height: 200px; 354 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 355 | bottom: -100px; 356 | right: -100px; 357 | animation: float 20s infinite alternate-reverse; 358 | } 359 | 360 | .shape-3 { 361 | width: 150px; 362 | height: 150px; 363 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 364 | top: 60%; 365 | left: -75px; 366 | animation: float 18s infinite alternate; 367 | } 368 | 369 | .shape-4 { 370 | width: 100px; 371 | height: 100px; 372 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 373 | top: 10%; 374 | right: -50px; 375 | animation: float 12s infinite alternate-reverse; 376 | } 377 | 378 | @keyframes float { 379 | 0% { 380 | transform: translate(0, 0) rotate(0deg); 381 | } 382 | 100% { 383 | transform: translate(30px, 30px) rotate(15deg); 384 | } 385 | } 386 | 387 | @media (max-width: 480px) { 388 | .email-card { 389 | padding: 30px 20px; 390 | border-radius: 12px; 391 | } 392 | 393 | .logo-circle { 394 | width: 60px; 395 | height: 60px; 396 | } 397 | 398 | .logo-icon { 399 | width: 30px; 400 | height: 30px; 401 | } 402 | 403 | .card-header h2 { 404 | font-size: 22px; 405 | } 406 | 407 | .card-header p { 408 | font-size: 14px; 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/emailInput.css: -------------------------------------------------------------------------------- 1 | .email-container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | min-height: 100vh; 6 | width: 100%; 7 | position: relative; 8 | overflow: hidden; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 10 | "Helvetica Neue", sans-serif; 11 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 12 | } 13 | 14 | .email-card { 15 | width: 100%; 16 | max-width: 420px; 17 | background: white; 18 | border-radius: 16px; 19 | padding: 40px 30px; 20 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); 21 | position: relative; 22 | z-index: 10; 23 | overflow: hidden; 24 | animation: cardAppear 0.6s ease-out; 25 | } 26 | 27 | @keyframes cardAppear { 28 | 0% { 29 | opacity: 0; 30 | transform: translateY(20px); 31 | } 32 | 100% { 33 | opacity: 1; 34 | transform: translateY(0); 35 | } 36 | } 37 | 38 | .card-header { 39 | text-align: center; 40 | margin-bottom: 30px; 41 | } 42 | 43 | .logo-container { 44 | display: flex; 45 | justify-content: center; 46 | margin-bottom: 20px; 47 | } 48 | 49 | .logo-circle { 50 | width: 70px; 51 | height: 70px; 52 | border-radius: 50%; 53 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | box-shadow: 0 10px 20px rgba(99, 102, 241, 0.3); 58 | animation: pulse 2s infinite; 59 | } 60 | 61 | @keyframes pulse { 62 | 0% { 63 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); 64 | } 65 | 70% { 66 | box-shadow: 0 0 0 15px rgba(99, 102, 241, 0); 67 | } 68 | 100% { 69 | box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); 70 | } 71 | } 72 | 73 | .logo-icon { 74 | width: 36px; 75 | height: 36px; 76 | fill: white; 77 | } 78 | 79 | .card-header h2 { 80 | font-size: 24px; 81 | font-weight: 700; 82 | color: #1f2937; 83 | margin: 0 0 8px 0; 84 | } 85 | 86 | .card-header p { 87 | font-size: 16px; 88 | color: #6b7280; 89 | margin: 0; 90 | } 91 | 92 | .input-group { 93 | margin-bottom: 30px; 94 | } 95 | 96 | .custom-input { 97 | position: relative; 98 | margin-bottom: 20px; 99 | } 100 | 101 | .custom-input input { 102 | width: 100%; 103 | height: 56px; 104 | padding: 20px 16px 8px; 105 | font-size: 16px; 106 | border: none; 107 | border-radius: 8px; 108 | background-color: #f3f4f6; 109 | color: #1f2937; 110 | transition: all 0.3s; 111 | outline: none; 112 | z-index: 1; 113 | position: relative; 114 | } 115 | 116 | .floating-label { 117 | position: absolute; 118 | left: 16px; 119 | top: 18px; 120 | font-size: 16px; 121 | color: #6b7280; 122 | transition: all 0.3s ease; 123 | pointer-events: none; 124 | z-index: 2; 125 | } 126 | 127 | .floating-label.active { 128 | top: 8px; 129 | font-size: 12px; 130 | color: #6366f1; 131 | } 132 | 133 | .custom-input.focused input { 134 | background-color: #f9fafb; 135 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 136 | } 137 | 138 | .custom-input.invalid input { 139 | background-color: #fee2e2; 140 | } 141 | 142 | .validation-icon { 143 | position: absolute; 144 | right: 16px; 145 | top: 50%; 146 | transform: translateY(-50%); 147 | width: 20px; 148 | height: 20px; 149 | z-index: 2; 150 | } 151 | 152 | .validation-icon svg { 153 | width: 100%; 154 | height: 100%; 155 | } 156 | 157 | .validation-icon.valid svg { 158 | fill: #10b981; 159 | } 160 | 161 | .validation-icon.invalid svg { 162 | fill: #ef4444; 163 | } 164 | 165 | .error-message, 166 | .validation-message { 167 | color: #ef4444; 168 | font-size: 14px; 169 | margin: 5px 0 0 0; 170 | animation: fadeIn 0.3s ease; 171 | } 172 | 173 | @keyframes fadeIn { 174 | from { 175 | opacity: 0; 176 | transform: translateY(-10px); 177 | } 178 | to { 179 | opacity: 1; 180 | transform: translateY(0); 181 | } 182 | } 183 | 184 | .sign-in-button { 185 | width: 100%; 186 | height: 56px; 187 | border: none; 188 | border-radius: 8px; 189 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 190 | color: white; 191 | font-size: 16px; 192 | font-weight: 600; 193 | cursor: pointer; 194 | position: relative; 195 | overflow: hidden; 196 | transition: all 0.3s; 197 | display: flex; 198 | align-items: center; 199 | justify-content: center; 200 | box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); 201 | } 202 | 203 | .sign-in-button:hover { 204 | transform: translateY(-2px); 205 | box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); 206 | } 207 | 208 | .sign-in-button:active { 209 | transform: translateY(0); 210 | box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); 211 | } 212 | 213 | .sign-in-button.disabled { 214 | background: linear-gradient(135deg, #a5a6f6 0%, #c4b5f8 100%); 215 | cursor: not-allowed; 216 | transform: none; 217 | box-shadow: none; 218 | } 219 | 220 | .button-text { 221 | transition: all 0.3s; 222 | display: flex; 223 | align-items: center; 224 | } 225 | 226 | .button-icon { 227 | width: 20px; 228 | height: 20px; 229 | margin-left: 8px; 230 | opacity: 0; 231 | transform: translateX(-10px); 232 | transition: all 0.3s; 233 | } 234 | 235 | .button-icon svg { 236 | fill: white; 237 | width: 100%; 238 | height: 100%; 239 | } 240 | 241 | .sign-in-button:hover .button-icon { 242 | opacity: 1; 243 | transform: translateX(0); 244 | } 245 | 246 | .sign-in-button.animating .button-text, 247 | .sign-in-button.animating .button-icon { 248 | opacity: 0; 249 | } 250 | 251 | .loading-spinner { 252 | position: absolute; 253 | width: 24px; 254 | height: 24px; 255 | border: 3px solid rgba(255, 255, 255, 0.3); 256 | border-radius: 50%; 257 | border-top-color: white; 258 | animation: spin 1s linear infinite; 259 | opacity: 0; 260 | transition: opacity 0.3s; 261 | } 262 | 263 | .sign-in-button.animating .loading-spinner { 264 | opacity: 1; 265 | } 266 | 267 | @keyframes spin { 268 | to { 269 | transform: rotate(360deg); 270 | } 271 | } 272 | 273 | .alternative-options { 274 | margin-top: 30px; 275 | } 276 | 277 | .divider { 278 | display: flex; 279 | align-items: center; 280 | margin: 20px 0; 281 | } 282 | 283 | .divider-line { 284 | flex-grow: 1; 285 | height: 1px; 286 | background-color: #e5e7eb; 287 | } 288 | 289 | .divider-text { 290 | padding: 0 15px; 291 | color: #9ca3af; 292 | font-size: 14px; 293 | } 294 | 295 | .social-buttons { 296 | display: flex; 297 | flex-direction: column; 298 | gap: 12px; 299 | } 300 | 301 | .social-button { 302 | display: flex; 303 | align-items: center; 304 | justify-content: center; 305 | height: 48px; 306 | border-radius: 8px; 307 | border: 1px solid #e5e7eb; 308 | background-color: white; 309 | font-size: 15px; 310 | font-weight: 500; 311 | color: #4b5563; 312 | cursor: pointer; 313 | transition: all 0.2s; 314 | } 315 | 316 | .social-button:hover { 317 | background-color: #f9fafb; 318 | border-color: #d1d5db; 319 | } 320 | 321 | .social-button svg { 322 | width: 20px; 323 | height: 20px; 324 | margin-right: 10px; 325 | } 326 | 327 | .social-button.google svg { 328 | fill: #ea4335; 329 | } 330 | 331 | .background-shapes { 332 | position: absolute; 333 | top: 0; 334 | left: 0; 335 | width: 100%; 336 | height: 100%; 337 | z-index: 1; 338 | overflow: hidden; 339 | } 340 | 341 | .shape { 342 | position: absolute; 343 | border-radius: 50%; 344 | opacity: 0.4; 345 | } 346 | 347 | .shape-1 { 348 | width: 300px; 349 | height: 300px; 350 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 351 | top: -150px; 352 | left: -150px; 353 | animation: float 15s infinite alternate; 354 | } 355 | 356 | .shape-2 { 357 | width: 200px; 358 | height: 200px; 359 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 360 | bottom: -100px; 361 | right: -100px; 362 | animation: float 20s infinite alternate-reverse; 363 | } 364 | 365 | .shape-3 { 366 | width: 150px; 367 | height: 150px; 368 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 369 | top: 60%; 370 | left: -75px; 371 | animation: float 18s infinite alternate; 372 | } 373 | 374 | .shape-4 { 375 | width: 100px; 376 | height: 100px; 377 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 378 | top: 10%; 379 | right: -50px; 380 | animation: float 12s infinite alternate-reverse; 381 | } 382 | 383 | @keyframes float { 384 | 0% { 385 | transform: translate(0, 0) rotate(0deg); 386 | } 387 | 100% { 388 | transform: translate(30px, 30px) rotate(15deg); 389 | } 390 | } 391 | 392 | @media (max-width: 480px) { 393 | .email-card { 394 | padding: 30px 20px; 395 | border-radius: 12px; 396 | } 397 | 398 | .logo-circle { 399 | width: 60px; 400 | height: 60px; 401 | } 402 | 403 | .logo-icon { 404 | width: 30px; 405 | height: 30px; 406 | } 407 | 408 | .card-header h2 { 409 | font-size: 22px; 410 | } 411 | 412 | .card-header p { 413 | font-size: 14px; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /components/dashboardUI.css: -------------------------------------------------------------------------------- 1 | .dashboard-container { 2 | min-height: 100vh; 3 | width: 100%; 4 | position: relative; 5 | overflow: hidden; 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", 7 | "Helvetica Neue", sans-serif; 8 | background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%); 9 | color: #1f2937; 10 | } 11 | 12 | .dashboard-background { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | z-index: 1; 19 | overflow: hidden; 20 | } 21 | 22 | .shape { 23 | position: absolute; 24 | border-radius: 50%; 25 | opacity: 0.4; 26 | } 27 | 28 | .dashboard-shape-1 { 29 | width: 400px; 30 | height: 400px; 31 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 32 | top: -200px; 33 | left: -200px; 34 | animation: float 20s infinite alternate; 35 | } 36 | 37 | .dashboard-shape-2 { 38 | width: 300px; 39 | height: 300px; 40 | background: linear-gradient(135deg, #8b5cf6 0%, #ec4899 100%); 41 | bottom: -150px; 42 | right: -150px; 43 | animation: float 25s infinite alternate-reverse; 44 | } 45 | 46 | .dashboard-shape-3 { 47 | width: 200px; 48 | height: 200px; 49 | background: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%); 50 | top: 70%; 51 | left: -100px; 52 | animation: float 22s infinite alternate; 53 | } 54 | 55 | .dashboard-shape-4 { 56 | width: 150px; 57 | height: 150px; 58 | background: linear-gradient(135deg, #f43f5e 0%, #6366f1 100%); 59 | top: 15%; 60 | right: -75px; 61 | animation: float 18s infinite alternate-reverse; 62 | } 63 | 64 | @keyframes float { 65 | 0% { 66 | transform: translate(0, 0) rotate(0deg); 67 | } 68 | 100% { 69 | transform: translate(40px, 40px) rotate(20deg); 70 | } 71 | } 72 | 73 | .dashboard-content { 74 | position: relative; 75 | z-index: 10; 76 | max-width: 1400px; 77 | margin: 0 auto; 78 | padding: 0 20px; 79 | } 80 | 81 | /* Header Styles */ 82 | .dashboard-header { 83 | display: flex; 84 | justify-content: space-between; 85 | align-items: center; 86 | padding: 20px 0; 87 | border-bottom: 1px solid rgba(229, 231, 235, 0.5); 88 | } 89 | 90 | .logo { 91 | display: flex; 92 | align-items: center; 93 | } 94 | 95 | .logo-circle { 96 | width: 40px; 97 | height: 40px; 98 | border-radius: 10px; 99 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | margin-right: 12px; 104 | box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); 105 | } 106 | 107 | .logo-icon { 108 | width: 24px; 109 | height: 24px; 110 | fill: white; 111 | } 112 | 113 | .logo-text { 114 | font-size: 20px; 115 | font-weight: 700; 116 | color: #1f2937; 117 | } 118 | 119 | .header-actions { 120 | display: flex; 121 | align-items: center; 122 | gap: 20px; 123 | } 124 | 125 | .notification-bell { 126 | position: relative; 127 | cursor: pointer; 128 | } 129 | 130 | .notification-bell svg { 131 | width: 24px; 132 | height: 24px; 133 | fill: #4b5563; 134 | transition: fill 0.2s; 135 | } 136 | 137 | .notification-bell:hover svg { 138 | fill: #6366f1; 139 | } 140 | 141 | .notification-badge { 142 | position: absolute; 143 | top: -5px; 144 | right: -5px; 145 | width: 18px; 146 | height: 18px; 147 | background-color: #ef4444; 148 | color: white; 149 | border-radius: 50%; 150 | font-size: 11px; 151 | font-weight: 600; 152 | display: flex; 153 | align-items: center; 154 | justify-content: center; 155 | border: 2px solid white; 156 | } 157 | 158 | .notification-dropdown { 159 | position: absolute; 160 | top: 40px; 161 | right: -10px; 162 | width: 320px; 163 | background: white; 164 | border-radius: 12px; 165 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); 166 | z-index: 100; 167 | overflow: hidden; 168 | animation: fadeInDown 0.3s ease; 169 | } 170 | 171 | @keyframes fadeInDown { 172 | from { 173 | opacity: 0; 174 | transform: translateY(-10px); 175 | } 176 | to { 177 | opacity: 1; 178 | transform: translateY(0); 179 | } 180 | } 181 | 182 | .notification-header { 183 | display: flex; 184 | justify-content: space-between; 185 | align-items: center; 186 | padding: 15px 20px; 187 | border-bottom: 1px solid #e5e7eb; 188 | } 189 | 190 | .notification-header h3 { 191 | font-size: 16px; 192 | font-weight: 600; 193 | margin: 0; 194 | } 195 | 196 | .mark-read { 197 | background: none; 198 | border: none; 199 | color: #6366f1; 200 | font-size: 13px; 201 | cursor: pointer; 202 | padding: 0; 203 | } 204 | 205 | .notification-list { 206 | max-height: 320px; 207 | overflow-y: auto; 208 | } 209 | 210 | .notification-item { 211 | display: flex; 212 | padding: 15px 20px; 213 | border-bottom: 1px solid #e5e7eb; 214 | transition: background-color 0.2s; 215 | } 216 | 217 | .notification-item:hover { 218 | background-color: #f9fafb; 219 | } 220 | 221 | .notification-item.unread { 222 | background-color: rgba(99, 102, 241, 0.05); 223 | } 224 | 225 | .notification-icon { 226 | width: 36px; 227 | height: 36px; 228 | border-radius: 50%; 229 | display: flex; 230 | align-items: center; 231 | justify-content: center; 232 | margin-right: 15px; 233 | flex-shrink: 0; 234 | } 235 | 236 | .notification-icon svg { 237 | width: 20px; 238 | height: 20px; 239 | fill: white; 240 | } 241 | 242 | .notification-icon.new { 243 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 244 | } 245 | 246 | .notification-icon.message { 247 | background: linear-gradient(135deg, #10b981 0%, #059669 100%); 248 | } 249 | 250 | .notification-icon.update { 251 | background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); 252 | } 253 | 254 | .notification-content { 255 | flex: 1; 256 | } 257 | 258 | .notification-content p { 259 | margin: 0 0 5px 0; 260 | font-size: 14px; 261 | color: #1f2937; 262 | } 263 | 264 | .notification-time { 265 | font-size: 12px; 266 | color: #6b7280; 267 | } 268 | 269 | .notification-footer { 270 | padding: 15px 20px; 271 | text-align: center; 272 | border-top: 1px solid #e5e7eb; 273 | } 274 | 275 | .notification-footer button { 276 | background: none; 277 | border: none; 278 | color: #6366f1; 279 | font-size: 14px; 280 | font-weight: 500; 281 | cursor: pointer; 282 | padding: 0; 283 | } 284 | 285 | .user-profile { 286 | display: flex; 287 | align-items: center; 288 | cursor: pointer; 289 | position: relative; 290 | } 291 | 292 | .user-avatar { 293 | width: 36px; 294 | height: 36px; 295 | border-radius: 50%; 296 | overflow: hidden; 297 | margin-right: 10px; 298 | background-color: #e5e7eb; 299 | } 300 | 301 | .user-avatar img { 302 | width: 100%; 303 | height: 100%; 304 | object-fit: cover; 305 | } 306 | 307 | .avatar-placeholder { 308 | width: 100%; 309 | height: 100%; 310 | display: flex; 311 | align-items: center; 312 | justify-content: center; 313 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 314 | color: white; 315 | font-weight: 600; 316 | font-size: 16px; 317 | } 318 | 319 | .user-name { 320 | font-size: 14px; 321 | font-weight: 500; 322 | color: #1f2937; 323 | margin-right: 5px; 324 | } 325 | 326 | .dropdown-arrow { 327 | width: 16px; 328 | height: 16px; 329 | fill: #6b7280; 330 | transition: transform 0.2s; 331 | } 332 | 333 | .dropdown-arrow.open { 334 | transform: rotate(180deg); 335 | } 336 | 337 | .user-dropdown { 338 | position: absolute; 339 | top: 45px; 340 | right: 0; 341 | width: 280px; 342 | background: white; 343 | border-radius: 12px; 344 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); 345 | z-index: 100; 346 | overflow: hidden; 347 | animation: fadeInDown 0.3s ease; 348 | } 349 | 350 | .user-dropdown-header { 351 | padding: 20px; 352 | display: flex; 353 | align-items: center; 354 | border-bottom: 1px solid #e5e7eb; 355 | } 356 | 357 | .user-dropdown-avatar { 358 | width: 50px; 359 | height: 50px; 360 | border-radius: 50%; 361 | overflow: hidden; 362 | margin-right: 15px; 363 | } 364 | 365 | .avatar-placeholder.large { 366 | font-size: 20px; 367 | } 368 | 369 | .user-dropdown-info { 370 | flex: 1; 371 | } 372 | 373 | .user-dropdown-info h4 { 374 | margin: 0 0 5px 0; 375 | font-size: 16px; 376 | font-weight: 600; 377 | color: #1f2937; 378 | } 379 | 380 | .user-dropdown-info p { 381 | margin: 0; 382 | font-size: 14px; 383 | color: #6b7280; 384 | word-break: break-all; 385 | } 386 | 387 | .user-dropdown-menu { 388 | padding: 10px 0; 389 | } 390 | 391 | .user-dropdown-item { 392 | display: flex; 393 | align-items: center; 394 | padding: 10px 20px; 395 | width: 100%; 396 | background: none; 397 | border: none; 398 | text-align: left; 399 | font-size: 14px; 400 | color: #4b5563; 401 | cursor: pointer; 402 | transition: background-color 0.2s; 403 | } 404 | 405 | .user-dropdown-item:hover { 406 | background-color: #f9fafb; 407 | color: #6366f1; 408 | } 409 | 410 | .user-dropdown-item svg { 411 | width: 20px; 412 | height: 20px; 413 | margin-right: 12px; 414 | fill: currentColor; 415 | } 416 | 417 | /* Main Content Styles */ 418 | .dashboard-main { 419 | padding: 30px 0; 420 | } 421 | 422 | .welcome-section { 423 | display: flex; 424 | justify-content: space-between; 425 | align-items: center; 426 | margin-bottom: 30px; 427 | } 428 | 429 | .welcome-text h1 { 430 | font-size: 28px; 431 | font-weight: 700; 432 | margin: 0 0 5px 0; 433 | color: #1f2937; 434 | } 435 | 436 | .welcome-text p { 437 | font-size: 16px; 438 | color: #6b7280; 439 | margin: 0; 440 | } 441 | 442 | .welcome-actions { 443 | display: flex; 444 | gap: 12px; 445 | } 446 | 447 | .action-button { 448 | display: flex; 449 | align-items: center; 450 | padding: 10px 16px; 451 | border-radius: 8px; 452 | font-size: 14px; 453 | font-weight: 500; 454 | cursor: pointer; 455 | transition: all 0.2s; 456 | border: none; 457 | } 458 | 459 | .action-button svg { 460 | width: 18px; 461 | height: 18px; 462 | margin-right: 8px; 463 | } 464 | 465 | .action-button.primary { 466 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 467 | color: white; 468 | box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); 469 | } 470 | 471 | .action-button.primary:hover { 472 | transform: translateY(-2px); 473 | box-shadow: 0 6px 15px rgba(99, 102, 241, 0.4); 474 | } 475 | 476 | .action-button.primary svg { 477 | fill: white; 478 | } 479 | 480 | .action-button.secondary { 481 | background: white; 482 | color: #4b5563; 483 | border: 1px solid #e5e7eb; 484 | } 485 | 486 | .action-button.secondary:hover { 487 | background: #f9fafb; 488 | color: #6366f1; 489 | } 490 | 491 | .action-button.secondary svg { 492 | fill: currentColor; 493 | } 494 | 495 | /* Stats Section */ 496 | .stats-section { 497 | display: grid; 498 | grid-template-columns: repeat(4, 1fr); 499 | gap: 20px; 500 | margin-bottom: 30px; 501 | } 502 | 503 | .stat-card { 504 | background: white; 505 | border-radius: 12px; 506 | padding: 20px; 507 | display: flex; 508 | align-items: center; 509 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); 510 | transition: transform 0.3s, box-shadow 0.3s; 511 | } 512 | 513 | .stat-card:hover { 514 | transform: translateY(-5px); 515 | box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); 516 | } 517 | 518 | .stat-icon { 519 | width: 50px; 520 | height: 50px; 521 | border-radius: 12px; 522 | display: flex; 523 | align-items: center; 524 | justify-content: center; 525 | margin-right: 15px; 526 | } 527 | 528 | .stat-icon svg { 529 | width: 24px; 530 | height: 24px; 531 | fill: white; 532 | } 533 | 534 | .stat-icon.visitors { 535 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 536 | } 537 | 538 | .stat-icon.revenue { 539 | background: linear-gradient(135deg, #10b981 0%, #059669 100%); 540 | } 541 | 542 | .stat-icon.orders { 543 | background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); 544 | } 545 | 546 | .stat-icon.conversions { 547 | background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); 548 | } 549 | 550 | .stat-content { 551 | flex: 1; 552 | } 553 | 554 | .stat-content h3 { 555 | font-size: 14px; 556 | font-weight: 500; 557 | color: #6b7280; 558 | margin: 0 0 5px 0; 559 | } 560 | 561 | .stat-value { 562 | font-size: 24px; 563 | font-weight: 700; 564 | color: #1f2937; 565 | margin-bottom: 5px; 566 | } 567 | 568 | .stat-change { 569 | font-size: 12px; 570 | font-weight: 500; 571 | } 572 | 573 | .stat-change.positive { 574 | color: #10b981; 575 | } 576 | 577 | .stat-change.negative { 578 | color: #ef4444; 579 | } 580 | 581 | /* Tabs Section */ 582 | .dashboard-tabs { 583 | background: white; 584 | border-radius: 12px; 585 | overflow: hidden; 586 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); 587 | } 588 | 589 | .tab-header { 590 | display: flex; 591 | border-bottom: 1px solid #e5e7eb; 592 | } 593 | 594 | .tab-button { 595 | padding: 15px 20px; 596 | background: none; 597 | border: none; 598 | font-size: 14px; 599 | font-weight: 500; 600 | color: #6b7280; 601 | cursor: pointer; 602 | transition: all 0.2s; 603 | position: relative; 604 | } 605 | 606 | .tab-button:hover { 607 | color: #6366f1; 608 | } 609 | 610 | .tab-button.active { 611 | color: #6366f1; 612 | } 613 | 614 | .tab-button.active::after { 615 | content: ""; 616 | position: absolute; 617 | bottom: -1px; 618 | left: 0; 619 | width: 100%; 620 | height: 2px; 621 | background: linear-gradient(90deg, #6366f1, #8b5cf6); 622 | } 623 | 624 | .tab-content { 625 | padding: 20px; 626 | min-height: 400px; 627 | } 628 | 629 | /* Overview Tab */ 630 | .overview-tab { 631 | display: grid; 632 | grid-template-columns: 2fr 1fr; 633 | gap: 20px; 634 | } 635 | 636 | .section-header { 637 | display: flex; 638 | justify-content: space-between; 639 | align-items: center; 640 | margin-bottom: 15px; 641 | } 642 | 643 | .section-header h2 { 644 | font-size: 18px; 645 | font-weight: 600; 646 | margin: 0; 647 | } 648 | 649 | .view-all { 650 | background: none; 651 | border: none; 652 | color: #6366f1; 653 | font-size: 14px; 654 | cursor: pointer; 655 | padding: 0; 656 | } 657 | 658 | .activity-list { 659 | display: flex; 660 | flex-direction: column; 661 | gap: 15px; 662 | } 663 | 664 | .activity-item { 665 | display: flex; 666 | align-items: flex-start; 667 | padding: 15px; 668 | background: #f9fafb; 669 | border-radius: 8px; 670 | transition: transform 0.2s; 671 | } 672 | 673 | .activity-item:hover { 674 | transform: translateX(5px); 675 | } 676 | 677 | .activity-icon { 678 | width: 36px; 679 | height: 36px; 680 | border-radius: 50%; 681 | display: flex; 682 | align-items: center; 683 | justify-content: center; 684 | margin-right: 15px; 685 | flex-shrink: 0; 686 | } 687 | 688 | .activity-icon svg { 689 | width: 20px; 690 | height: 20px; 691 | fill: white; 692 | } 693 | 694 | .activity-icon.login { 695 | background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); 696 | } 697 | 698 | .activity-icon.update { 699 | background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); 700 | } 701 | 702 | .activity-icon.project { 703 | background: linear-gradient(135deg, #10b981 0%, #059669 100%); 704 | } 705 | 706 | .activity-content { 707 | flex: 1; 708 | } 709 | 710 | .activity-content h4 { 711 | font-size: 15px; 712 | font-weight: 600; 713 | margin: 0 0 5px 0; 714 | color: #1f2937; 715 | } 716 | 717 | .activity-content p { 718 | font-size: 14px; 719 | color: #6b7280; 720 | margin: 0 0 5px 0; 721 | } 722 | 723 | .activity-time { 724 | font-size: 12px; 725 | color: #9ca3af; 726 | } 727 | 728 | .action-grid { 729 | display: grid; 730 | grid-template-columns: repeat(2, 1fr); 731 | gap: 15px; 732 | } 733 | 734 | .action-card { 735 | display: flex; 736 | flex-direction: column; 737 | align-items: center; 738 | justify-content: center; 739 | padding: 20px; 740 | background: #f9fafb; 741 | border-radius: 8px; 742 | border: none; 743 | cursor: pointer; 744 | transition: all 0.2s; 745 | } 746 | 747 | .action-card:hover { 748 | background: #f3f4f6; 749 | transform: translateY(-3px); 750 | } 751 | 752 | .action-card svg { 753 | width: 24px; 754 | height: 24px; 755 | fill: #6366f1; 756 | margin-bottom: 10px; 757 | } 758 | 759 | .action-card span { 760 | font-size: 14px; 761 | font-weight: 500; 762 | color: #4b5563; 763 | } 764 | 765 | /* Placeholder Tabs */ 766 | .analytics-placeholder, 767 | .projects-placeholder, 768 | .settings-placeholder { 769 | display: flex; 770 | flex-direction: column; 771 | align-items: center; 772 | justify-content: center; 773 | height: 300px; 774 | text-align: center; 775 | } 776 | 777 | .placeholder-icon { 778 | width: 60px; 779 | height: 60px; 780 | fill: #d1d5db; 781 | margin-bottom: 20px; 782 | } 783 | 784 | .analytics-placeholder h3, 785 | .projects-placeholder h3, 786 | .settings-placeholder h3 { 787 | font-size: 18px; 788 | font-weight: 600; 789 | color: #4b5563; 790 | margin: 0 0 10px 0; 791 | } 792 | 793 | .analytics-placeholder p, 794 | .projects-placeholder p, 795 | .settings-placeholder p { 796 | font-size: 14px; 797 | color: #9ca3af; 798 | margin: 0; 799 | } 800 | 801 | /* Responsive Styles */ 802 | @media (max-width: 1024px) { 803 | .stats-section { 804 | grid-template-columns: repeat(2, 1fr); 805 | } 806 | 807 | .overview-tab { 808 | grid-template-columns: 1fr; 809 | } 810 | } 811 | 812 | @media (max-width: 768px) { 813 | .welcome-section { 814 | flex-direction: column; 815 | align-items: flex-start; 816 | gap: 20px; 817 | } 818 | 819 | .dashboard-header { 820 | flex-direction: column; 821 | gap: 15px; 822 | align-items: flex-start; 823 | } 824 | 825 | .header-actions { 826 | width: 100%; 827 | justify-content: space-between; 828 | } 829 | 830 | .user-name { 831 | display: none; 832 | } 833 | } 834 | 835 | @media (max-width: 640px) { 836 | .stats-section { 837 | grid-template-columns: 1fr; 838 | } 839 | 840 | .action-grid { 841 | grid-template-columns: 1fr; 842 | } 843 | 844 | .tab-button { 845 | padding: 12px 15px; 846 | font-size: 13px; 847 | } 848 | } 849 | 850 | .welcome-section.centered { 851 | display: flex; 852 | flex-direction: column; 853 | align-items: center; 854 | justify-content: center; 855 | text-align: center; 856 | min-height: 60vh; 857 | padding: 2rem; 858 | } 859 | 860 | .large-greeting { 861 | font-size: 3.5rem; 862 | font-weight: 600; 863 | margin: 0; 864 | color: #1a1a1a; 865 | line-height: 1.2; 866 | } 867 | 868 | .date-text { 869 | font-size: 1.5rem; 870 | color: #666; 871 | margin-top: 1rem; 872 | } 873 | 874 | .logout-button { 875 | display: flex; 876 | align-items: center; 877 | gap: 8px; 878 | padding: 8px 16px; 879 | background: #f3f4f6; 880 | border: none; 881 | border-radius: 8px; 882 | color: #4b5563; 883 | font-size: 14px; 884 | font-weight: 500; 885 | cursor: pointer; 886 | transition: all 0.2s; 887 | } 888 | 889 | .logout-button:hover { 890 | background: #e5e7eb; 891 | } 892 | 893 | .logout-button svg { 894 | width: 16px; 895 | height: 16px; 896 | fill: currentColor; 897 | } 898 | --------------------------------------------------------------------------------