├── 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�� 0PLTE Z? 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 | 
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 | ���� JFIF H H �� �Exif MM * J R( �i Z H H � � � �� 8Photoshop 3.0 8BIM 8BIM% ��ُ �� ���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��0 VFbP��!
6 | Io40 ��[?p #�|�@ !.E� 3��4p Bq �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 |
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 |
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 |
54 |
55 |
56 |
57 |
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 |
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 |
77 | ) : (
78 |
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 |
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 |
69 | ) : (
70 |
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 |
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 |
93 | ) : (
94 |
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 |
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 |
--------------------------------------------------------------------------------