MrNoOne07 commited on
Commit
046198b
·
0 Parent(s):

Initial commit - Manoj Guttikonda portfolio

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +32 -0
  2. .prettierignore +8 -0
  3. .prettierrc +6 -0
  4. Dockerfile +15 -0
  5. README.md +211 -0
  6. bun.lock +0 -0
  7. bunfig.toml +6 -0
  8. components.json +22 -0
  9. docs/PROJECT_STRUCTURE.md +36 -0
  10. eslint.config.js +40 -0
  11. package-lock.json +0 -0
  12. package.json +88 -0
  13. public/favicon.svg +5 -0
  14. scripts/windows/run-dev-live.cmd +4 -0
  15. scripts/windows/run-dev.cmd +4 -0
  16. src/assets/manoj.jpg +0 -0
  17. src/components/portfolio/README.md +27 -0
  18. src/components/portfolio/layout/SiteNav.tsx +96 -0
  19. src/components/portfolio/legacy/UnusedAboutSection.tsx +34 -0
  20. src/components/portfolio/sections/ContactSection.tsx +42 -0
  21. src/components/portfolio/sections/CredentialsSection.tsx +116 -0
  22. src/components/portfolio/sections/EducationSection.tsx +102 -0
  23. src/components/portfolio/sections/FeaturedProjectsSection.tsx +449 -0
  24. src/components/portfolio/sections/HeroSection.tsx +228 -0
  25. src/components/portfolio/sections/JourneySection.tsx +196 -0
  26. src/components/portfolio/sections/SkillsSection.tsx +47 -0
  27. src/components/ui/accordion.tsx +51 -0
  28. src/components/ui/alert-dialog.tsx +115 -0
  29. src/components/ui/alert.tsx +49 -0
  30. src/components/ui/aspect-ratio.tsx +5 -0
  31. src/components/ui/avatar.tsx +47 -0
  32. src/components/ui/badge.tsx +32 -0
  33. src/components/ui/breadcrumb.tsx +101 -0
  34. src/components/ui/button.tsx +49 -0
  35. src/components/ui/calendar.tsx +177 -0
  36. src/components/ui/card.tsx +55 -0
  37. src/components/ui/carousel.tsx +240 -0
  38. src/components/ui/chart.tsx +331 -0
  39. src/components/ui/checkbox.tsx +26 -0
  40. src/components/ui/collapsible.tsx +11 -0
  41. src/components/ui/command.tsx +143 -0
  42. src/components/ui/context-menu.tsx +187 -0
  43. src/components/ui/dialog.tsx +104 -0
  44. src/components/ui/drawer.tsx +98 -0
  45. src/components/ui/dropdown-menu.tsx +188 -0
  46. src/components/ui/form.tsx +171 -0
  47. src/components/ui/hover-card.tsx +27 -0
  48. src/components/ui/input-otp.tsx +69 -0
  49. src/components/ui/input.tsx +22 -0
  50. src/components/ui/label.tsx +21 -0
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ .output
14
+ .vinxi
15
+ .tanstack/**
16
+ .nitro
17
+ *.local
18
+
19
+ # Wrangler / Cloudflare
20
+ .wrangler/
21
+ .dev.vars
22
+
23
+ # Editor directories and files
24
+ .vscode/*
25
+ !.vscode/extensions.json
26
+ .idea
27
+ .DS_Store
28
+ *.suo
29
+ *.ntvs*
30
+ *.njsproj
31
+ *.sln
32
+ *.sw?
.prettierignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .output
4
+ .vinxi
5
+ pnpm-lock.yaml
6
+ package-lock.json
7
+ bun.lock
8
+ routeTree.gen.ts
.prettierrc ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 100,
3
+ "semi": true,
4
+ "singleQuote": false,
5
+ "trailingComma": "all"
6
+ }
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM oven/bun:1-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json bun.lock bunfig.toml ./
6
+ RUN bun install --frozen-lockfile
7
+
8
+ COPY . .
9
+ RUN bun run build
10
+
11
+ EXPOSE 7860
12
+ ENV PORT=7860
13
+ ENV HOST=0.0.0.0
14
+
15
+ CMD ["node", ".output/server/index.mjs"]
README.md ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manoj Guttikonda — Portfolio
2
+
3
+ Personal portfolio website for **Manoj Guttikonda** — MS Business Analytics @ UNT, ex-Accenture Cloud Operations Engineer.
4
+
5
+ Live at: _[your deployed URL here]_
6
+
7
+ ---
8
+
9
+ ## Tech Stack
10
+
11
+ | Layer | Technology |
12
+ |---|---|
13
+ | Framework | React 19 + TanStack Start (SSR) |
14
+ | Routing | TanStack Router |
15
+ | Styling | Tailwind CSS v4 |
16
+ | Animations | Framer Motion |
17
+ | Icons | Lucide React |
18
+ | UI Components | shadcn/ui (Radix UI) |
19
+ | Font (body) | Carlito (Calibri-equivalent, Google Fonts) |
20
+ | Font (display) | Bebas Neue (Google Fonts) |
21
+ | Package manager | Bun |
22
+ | Build tool | Vite + Nitro |
23
+
24
+ ---
25
+
26
+ ## Project Structure
27
+
28
+ ```
29
+ src/
30
+ ├── assets/
31
+ │ └── manoj.jpg # Profile photo
32
+ ├── components/
33
+ │ ├── portfolio/ # All portfolio section components
34
+ │ │ ├── Nav.tsx # Navigation bar (desktop + mobile)
35
+ │ │ ├── Hero.tsx # Hero section (name, photo, intro)
36
+ │ │ ├── Timeline.tsx # Career journey (undergrad → Accenture → UNT)
37
+ │ │ ├── Projects.tsx # Featured projects + research publications
38
+ │ │ ├── Skills.tsx # Skills toolkit (4 categories)
39
+ │ │ ├── Education.tsx # Education + internships
40
+ │ │ ├── Credentials.tsx # Certifications, honors, campus experience
41
+ │ │ ├── About.tsx # About section
42
+ │ │ └── Contact.tsx # Contact section
43
+ │ └── ui/ # shadcn/ui base components (do not edit)
44
+ ├── hooks/
45
+ │ └── use-mobile.tsx # Mobile breakpoint hook
46
+ ├── lib/
47
+ │ ├── links.ts # All URLs (email, resume, GitHub, LinkedIn, HF)
48
+ │ ├── error-capture.ts # Error capture utility
49
+ │ ├── error-page.ts # SSR error page renderer
50
+ │ ├── lovable-error-reporting.ts # Error reporting (Lovable platform)
51
+ │ └── utils.ts # Tailwind class merge utility
52
+ ├── routes/
53
+ │ ├── __root.tsx # Root layout + global head meta
54
+ │ └── index.tsx # Home page (assembles all sections)
55
+ ├── router.tsx # TanStack Router setup
56
+ ├── server.ts # SSR server entry
57
+ ├── start.ts # App start entry
58
+ └── styles.css # Global CSS + Tailwind theme + custom fonts
59
+ public/
60
+ └── favicon.svg # MG favicon (navy + orange)
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Customising Content
66
+
67
+ All personalisation lives in just a few files:
68
+
69
+ ### 1. Links & URLs — `src/lib/links.ts`
70
+ ```ts
71
+ export const CONTACT_EMAIL = "your@email.com";
72
+ export const RESUME_URL = "https://...";
73
+ export const LINKEDIN_URL = "https://linkedin.com/in/yourhandle";
74
+ export const GITHUB_URL = "https://github.com/yourhandle";
75
+ export const HUGGING_FACE_URL = "https://huggingface.co/yourhandle";
76
+ ```
77
+
78
+ ### 2. Photo — `src/assets/manoj.jpg`
79
+ Replace with your own photo. Keep the filename or update the import in `Hero.tsx`.
80
+
81
+ ### 3. Hero section — `src/components/portfolio/Hero.tsx`
82
+ - Name, tagline, bio paragraphs
83
+ - Role badges
84
+ - Metric strip (GPA, months of experience, etc.)
85
+
86
+ ### 4. Projects — `src/components/portfolio/Projects.tsx`
87
+ Each project is an object in the `projects` array:
88
+ ```ts
89
+ {
90
+ id: "unique-id",
91
+ era: "UNT - Graduate" | "Accenture" | "Undergrad" | "Independent",
92
+ title: "Project Title",
93
+ tagline: "One-line description.",
94
+ period: "Jan 2025 - May 2025",
95
+ details: ["bullet 1", "bullet 2", "bullet 3"],
96
+ stack: ["Python", "AI"],
97
+ links: [{ label: "View Code", href: "https://github.com/..." }],
98
+ }
99
+ ```
100
+
101
+ ### 5. Skills — `src/components/portfolio/Skills.tsx`
102
+ Edit the `groups` array to add/remove skills.
103
+
104
+ ### 6. Timeline — `src/components/portfolio/Timeline.tsx`
105
+ Edit the `items` array for career milestones.
106
+
107
+ ### 7. Education — `src/components/portfolio/Education.tsx`
108
+ Edit `edu` and `internships` arrays.
109
+
110
+ ### 8. Credentials — `src/components/portfolio/Credentials.tsx`
111
+ Edit `certs`, `honors`, `leadership`, and `campusExperience` arrays.
112
+
113
+ ### 9. Global styles — `src/styles.css`
114
+ - `html { font-size: 19px; }` — base font scale
115
+ - `--font-sans` — body font
116
+ - `--font-display` — heading font (Bebas Neue)
117
+ - Color tokens: `--navy`, `--accent`, `--chakra`
118
+
119
+ ### 10. SEO meta — `src/routes/index.tsx`
120
+ Update `pageTitle` and `pageDescription` constants.
121
+
122
+ ---
123
+
124
+ ## Local Development
125
+
126
+ ```bash
127
+ # Install dependencies
128
+ bun install
129
+
130
+ # Start dev server
131
+ bun run dev
132
+ # Open http://localhost:5173
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Deployment — HuggingFace Spaces
138
+
139
+ ### Option A: Docker Space (Recommended)
140
+
141
+ 1. Create a new Space on [huggingface.co/spaces](https://huggingface.co/spaces)
142
+ 2. Choose **Docker** as the Space SDK
143
+ 3. Clone the Space repo and copy this project into it
144
+ 4. Add the `Dockerfile` (included in this repo) at the root
145
+ 5. Push — HuggingFace builds and deploys automatically
146
+
147
+ The `Dockerfile` installs Bun, builds the app, and serves it on port **7860**.
148
+
149
+ ### Option B: Static Build
150
+
151
+ If you want a pure static output (no server), change `vite.config.ts`:
152
+
153
+ ```ts
154
+ export default defineConfig({
155
+ tanstackStart: {
156
+ server: { entry: "server" },
157
+ nitro: { preset: "static" }, // add this line
158
+ },
159
+ });
160
+ ```
161
+
162
+ Then:
163
+ ```bash
164
+ bun run build
165
+ # Upload contents of .output/public/ to a Static Space or any static host
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Deployment — Other Platforms
171
+
172
+ | Platform | How |
173
+ |---|---|
174
+ | Vercel | Connect GitHub repo, auto-detected as Vite project |
175
+ | Netlify | `bun run build`, publish `.output/public/` |
176
+ | Cloudflare Pages | Default build target is already `cloudflare` |
177
+
178
+ ---
179
+
180
+ ## LinkedIn Profile Update
181
+
182
+ Add the following to your LinkedIn:
183
+
184
+ - **Portfolio URL** in the intro/contact section: your deployed URL
185
+ - **Featured section**: screenshot of the portfolio homepage, link to live site
186
+ - **About section**: mirror the hero bio text
187
+ - **Skills section**: pull from `Skills.tsx` groups
188
+ - **Projects section**: add each project from `Projects.tsx` with GitHub links
189
+
190
+ ---
191
+
192
+ ## OG Image (Social Share Preview)
193
+
194
+ Add a `1200 × 630 px` image named `og.jpg` to the `/public` folder.
195
+ When someone shares your portfolio link on LinkedIn or Twitter, this image shows as the preview thumbnail.
196
+
197
+ ---
198
+
199
+ ## Current Portfolio Details
200
+
201
+ | Item | Value |
202
+ |---|---|
203
+ | Name | Manoj Guttikonda |
204
+ | Email | guttikondamanoj7@gmail.com |
205
+ | LinkedIn | linkedin.com/in/manojguttikonda |
206
+ | GitHub | github.com/im-mj |
207
+ | HuggingFace | huggingface.co/MrNoOne07 |
208
+ | Degree | MS Business Analytics, UNT (Aug 2024 – May 2026), GPA 3.8 |
209
+ | Experience | Cloud Ops Engineer @ Accenture, Oct 2021 – Jun 2024 (33 months) |
210
+ | Certifications | AZ-104, AZ-500, Azure Fundamentals, Palo Alto, VMware, AWS |
211
+ | Research | AI-Driven Malicious URL Detection — BIGS 2025 & SWDSI 2026 |
bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
bunfig.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [install]
2
+ # 24h supply-chain guard: skip package versions published less than a day ago.
3
+ minimumReleaseAge = 86400
4
+ # Each entry bypasses the 24h guard for one package - confirm with the user
5
+ # before adding any.
6
+ minimumReleaseAgeExcludes = ["@lovable.dev/vite-tanstack-config"]
components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "css": "src/styles.css",
8
+ "baseColor": "slate",
9
+ "cssVariables": true,
10
+ "prefix": ""
11
+ },
12
+ "iconLibrary": "lucide",
13
+ "rtl": false,
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
docs/PROJECT_STRUCTURE.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Structure
2
+
3
+ This app is a Vite/TanStack React portfolio. Some files must stay in the project root because the build tools look for them there.
4
+
5
+ ## Root Files
6
+
7
+ - `package.json` - npm scripts and dependencies.
8
+ - `package-lock.json` - exact installed dependency versions for npm.
9
+ - `bun.lock`, `bunfig.toml` - Bun dependency settings kept from the original export.
10
+ - `vite.config.ts` - Vite app configuration.
11
+ - `tsconfig.json` - TypeScript configuration.
12
+ - `eslint.config.js` - linting configuration.
13
+ - `components.json` - shadcn/ui component configuration.
14
+ - `.gitignore`, `.prettierignore`, `.prettierrc` - repo and formatting settings.
15
+ - `Dockerfile` - container build settings.
16
+ - `README.md` - app notes and setup.
17
+
18
+ ## Folders
19
+
20
+ - `src/` - actual portfolio source code.
21
+ - `src/components/portfolio/` - custom portfolio UI, organized by layout and sections.
22
+ - `src/components/ui/` - reusable design-system components.
23
+ - `src/assets/` - local images used by the app.
24
+ - `public/` - public static files such as favicon.
25
+ - `scripts/windows/` - Windows scripts for starting the local dev server.
26
+ - `logs/` - local dev-server logs.
27
+ - `docs/` - documentation about this project.
28
+
29
+ ## Generated Folders
30
+
31
+ These are created by tools and should usually not be edited manually:
32
+
33
+ - `node_modules/` - installed npm packages.
34
+ - `dist/` - production build output.
35
+ - `.tanstack/` - TanStack generated runtime files.
36
+ - Original platform metadata was archived to `workspace-docs/legacy-lovable-project-metadata/` because the local portfolio does not need it to run.
eslint.config.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import eslintPluginPrettier from "eslint-plugin-prettier/recommended";
3
+ import globals from "globals";
4
+ import reactHooks from "eslint-plugin-react-hooks";
5
+ import reactRefresh from "eslint-plugin-react-refresh";
6
+ import tseslint from "typescript-eslint";
7
+
8
+ export default tseslint.config(
9
+ { ignores: ["dist", ".output", ".vinxi"] },
10
+ {
11
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
12
+ files: ["**/*.{ts,tsx}"],
13
+ languageOptions: {
14
+ ecmaVersion: 2020,
15
+ globals: globals.browser,
16
+ },
17
+ plugins: {
18
+ "react-hooks": reactHooks,
19
+ "react-refresh": reactRefresh,
20
+ },
21
+ rules: {
22
+ ...reactHooks.configs.recommended.rules,
23
+ "no-restricted-imports": [
24
+ "error",
25
+ {
26
+ paths: [
27
+ {
28
+ name: "server-only",
29
+ message:
30
+ "TanStack Start does not use the Next.js `server-only` package. Rename the module to `*.server.ts` or mark it with `@tanstack/react-start/server-only`.",
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
36
+ "@typescript-eslint/no-unused-vars": "off",
37
+ },
38
+ },
39
+ eslintPluginPrettier,
40
+ );
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tanstack_start_ts",
3
+ "private": true,
4
+ "sideEffects": false,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "preview": "vite preview",
11
+ "lint": "eslint .",
12
+ "format": "prettier --write ."
13
+ },
14
+ "dependencies": {
15
+ "@hookform/resolvers": "^5.2.2",
16
+ "@radix-ui/react-accordion": "^1.2.12",
17
+ "@radix-ui/react-alert-dialog": "^1.1.15",
18
+ "@radix-ui/react-aspect-ratio": "^1.1.8",
19
+ "@radix-ui/react-avatar": "^1.1.11",
20
+ "@radix-ui/react-checkbox": "^1.3.3",
21
+ "@radix-ui/react-collapsible": "^1.1.12",
22
+ "@radix-ui/react-context-menu": "^2.2.16",
23
+ "@radix-ui/react-dialog": "^1.1.15",
24
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
25
+ "@radix-ui/react-hover-card": "^1.1.15",
26
+ "@radix-ui/react-label": "^2.1.8",
27
+ "@radix-ui/react-menubar": "^1.1.16",
28
+ "@radix-ui/react-navigation-menu": "^1.2.14",
29
+ "@radix-ui/react-popover": "^1.1.15",
30
+ "@radix-ui/react-progress": "^1.1.8",
31
+ "@radix-ui/react-radio-group": "^1.3.8",
32
+ "@radix-ui/react-scroll-area": "^1.2.10",
33
+ "@radix-ui/react-select": "^2.2.6",
34
+ "@radix-ui/react-separator": "^1.1.8",
35
+ "@radix-ui/react-slider": "^1.3.6",
36
+ "@radix-ui/react-slot": "^1.2.4",
37
+ "@radix-ui/react-switch": "^1.2.6",
38
+ "@radix-ui/react-tabs": "^1.1.13",
39
+ "@radix-ui/react-toggle": "^1.1.10",
40
+ "@radix-ui/react-toggle-group": "^1.1.11",
41
+ "@radix-ui/react-tooltip": "^1.2.8",
42
+ "@tailwindcss/vite": "^4.2.1",
43
+ "@tanstack/react-query": "^5.83.0",
44
+ "@tanstack/react-router": "^1.168.25",
45
+ "@tanstack/react-start": "^1.167.50",
46
+ "@tanstack/router-plugin": "^1.167.28",
47
+ "class-variance-authority": "^0.7.1",
48
+ "clsx": "^2.1.1",
49
+ "cmdk": "^1.1.1",
50
+ "date-fns": "^4.1.0",
51
+ "embla-carousel-react": "^8.6.0",
52
+ "framer-motion": "^12.40.0",
53
+ "input-otp": "^1.4.2",
54
+ "lucide-react": "^0.575.0",
55
+ "react": "^19.2.0",
56
+ "react-day-picker": "^9.14.0",
57
+ "react-dom": "^19.2.0",
58
+ "react-hook-form": "^7.71.2",
59
+ "react-resizable-panels": "^4.6.5",
60
+ "recharts": "^2.15.4",
61
+ "sonner": "^2.0.7",
62
+ "tailwind-merge": "^3.5.0",
63
+ "tailwindcss": "^4.2.1",
64
+ "tw-animate-css": "^1.3.4",
65
+ "vaul": "^1.1.2",
66
+ "vite-tsconfig-paths": "^6.0.2",
67
+ "zod": "^3.24.2"
68
+ },
69
+ "devDependencies": {
70
+ "@eslint/js": "^9.32.0",
71
+ "@lovable.dev/vite-tanstack-config": "^2.1.1",
72
+ "@types/node": "^22.16.5",
73
+ "@types/react": "^19.2.0",
74
+ "@types/react-dom": "^19.2.0",
75
+ "@vitejs/plugin-react": "^5.0.4",
76
+ "eslint": "^9.32.0",
77
+ "eslint-config-prettier": "^10.1.1",
78
+ "eslint-plugin-prettier": "^5.2.6",
79
+ "eslint-plugin-react-hooks": "^5.2.0",
80
+ "eslint-plugin-react-refresh": "^0.4.20",
81
+ "globals": "^15.15.0",
82
+ "nitro": "3.0.260429-beta",
83
+ "prettier": "^3.7.3",
84
+ "typescript": "^5.8.3",
85
+ "typescript-eslint": "^8.56.1",
86
+ "vite": "^7.3.1"
87
+ }
88
+ }
public/favicon.svg ADDED
scripts/windows/run-dev-live.cmd ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0..\.."
3
+ set "PATH=C:\Program Files\nodejs;%PATH%"
4
+ "C:\Program Files\nodejs\npm.cmd" run dev -- --host 127.0.0.1 --port 5173
scripts/windows/run-dev.cmd ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ @echo off
2
+ cd /d "%~dp0..\.."
3
+ set "PATH=C:\Program Files\nodejs;%PATH%"
4
+ "C:\Program Files\nodejs\npm.cmd" run dev -- --host 127.0.0.1 --port 5173 > logs\dev-server.log 2> logs\dev-server.err.log
src/assets/manoj.jpg ADDED
src/components/portfolio/README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Portfolio Component Folders
2
+
3
+ This folder contains the custom portfolio UI. The shared design-system components live separately in `src/components/ui`.
4
+
5
+ ## `layout`
6
+
7
+ Page-level layout pieces that frame the portfolio.
8
+
9
+ - `SiteNav.tsx` - fixed header, desktop links, mobile menu, resume button.
10
+
11
+ ## `sections`
12
+
13
+ Rendered page sections, in the same order used by `src/routes/index.tsx`.
14
+
15
+ - `HeroSection.tsx` - home intro, profile photo, proof stats, primary CTAs.
16
+ - `JourneySection.tsx` - animated three-step journey timeline.
17
+ - `FeaturedProjectsSection.tsx` - profile links, research banner, project cards.
18
+ - `SkillsSection.tsx` - skills/toolkit groups.
19
+ - `EducationSection.tsx` - degrees and internships only.
20
+ - `CredentialsSection.tsx` - certifications, honors, leadership, campus jobs.
21
+ - `ContactSection.tsx` - email and resume contact actions.
22
+
23
+ ## `legacy`
24
+
25
+ Unused components kept only for reference during cleanup.
26
+
27
+ - `UnusedAboutSection.tsx` - old standalone About section. The about copy is now merged into the hero.
src/components/portfolio/layout/SiteNav.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { Menu, X } from "lucide-react";
3
+ import { RESUME_URL } from "@/lib/links";
4
+
5
+ const links = [
6
+ { href: "#home", label: "Home" },
7
+ { href: "#journey", label: "Journey" },
8
+ { href: "#projects", label: "Projects" },
9
+ { href: "#skills", label: "Skills" },
10
+ { href: "#education", label: "Education" },
11
+ { href: "#contact", label: "Contact" },
12
+ ];
13
+
14
+ export function SiteNav() {
15
+ const [scrolled, setScrolled] = useState(false);
16
+ const [open, setOpen] = useState(false);
17
+
18
+ useEffect(() => {
19
+ const onScroll = () => setScrolled(window.scrollY > 20);
20
+ onScroll();
21
+ window.addEventListener("scroll", onScroll, { passive: true });
22
+ return () => window.removeEventListener("scroll", onScroll);
23
+ }, []);
24
+
25
+ return (
26
+ <header
27
+ className={`fixed top-0 inset-x-0 z-50 transition-all ${
28
+ scrolled || open
29
+ ? "backdrop-blur-md bg-background/75 border-b border-border/60"
30
+ : "bg-transparent"
31
+ }`}
32
+ >
33
+ <nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 lg:px-10">
34
+ <a
35
+ href="#home"
36
+ onClick={() => setOpen(false)}
37
+ className="flex items-center gap-1.5 font-display text-4xl tracking-wide text-navy"
38
+ aria-label="Go to home"
39
+ >
40
+ <span className="inline-flex h-9 w-9 items-center justify-center text-[2.35rem] leading-none text-foreground animate-heartbeat">
41
+
42
+ </span>
43
+ <span>MG</span>
44
+ </a>
45
+
46
+ <ul className="hidden items-center gap-7 text-base font-bold text-muted-foreground md:flex">
47
+ {links.map((l) => (
48
+ <li key={l.href}>
49
+ <a href={l.href} className="hover:text-accent transition-colors">
50
+ {l.label}
51
+ </a>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+
56
+ <div className="flex items-center gap-2">
57
+ <a
58
+ href={RESUME_URL}
59
+ target="_blank"
60
+ rel="noreferrer"
61
+ className="inline-flex items-center rounded-full bg-navy px-5 py-2 text-sm font-semibold text-primary-foreground hover:bg-accent transition-colors"
62
+ >
63
+ Resume
64
+ </a>
65
+ <button
66
+ type="button"
67
+ onClick={() => setOpen((current) => !current)}
68
+ className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-border bg-card text-navy transition-colors hover:border-accent hover:text-accent md:hidden"
69
+ aria-label={open ? "Close navigation menu" : "Open navigation menu"}
70
+ aria-expanded={open}
71
+ >
72
+ {open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
73
+ </button>
74
+ </div>
75
+ </nav>
76
+
77
+ {open && (
78
+ <div className="border-t border-border/60 bg-background/95 px-6 py-4 shadow-lg md:hidden">
79
+ <ul className="mx-auto grid max-w-7xl gap-2 text-sm font-semibold text-navy">
80
+ {links.map((l) => (
81
+ <li key={l.href}>
82
+ <a
83
+ href={l.href}
84
+ onClick={() => setOpen(false)}
85
+ className="block rounded-xl px-4 py-3 transition-colors hover:bg-accent/10 hover:text-accent"
86
+ >
87
+ {l.label}
88
+ </a>
89
+ </li>
90
+ ))}
91
+ </ul>
92
+ </div>
93
+ )}
94
+ </header>
95
+ );
96
+ }
src/components/portfolio/legacy/UnusedAboutSection.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function UnusedAboutSection() {
2
+ return (
3
+ <section id="about" className="py-24">
4
+ <div className="mx-auto max-w-7xl px-6 grid md:grid-cols-[auto_1fr] gap-10 items-start">
5
+ <div>
6
+ <p className="font-display text-accent text-2xl tracking-widest">ABOUT</p>
7
+ <h2 className="mt-2 font-display text-5xl text-navy">Hello!</h2>
8
+ </div>
9
+ <div className="space-y-5 text-foreground/85 leading-relaxed tracking-wide">
10
+ <p>
11
+ I'm the type of human being that loves to make messy data behave and turn
12
+ complicated stuff into the moments where it was actually logical all along.
13
+ </p>
14
+ <p>
15
+ My first job was in <span className="font-semibold text-navy">cloud + network security at Accenture</span>
16
+ {" "}(33 months), working with enterprise firewalls and the behind-the-scenes
17
+ systems that keep things safe. Somewhere in the middle of that path I got hooked
18
+ on the <em>reason behind the numbers</em> - and now I'm pursuing my{" "}
19
+ <span className="font-semibold text-navy">MS in Business Analytics at UNT</span>,
20
+ building projects that combine analytics, automation, and AI.
21
+ </p>
22
+ <p>
23
+ I went so far as to write an automation script that cut manual operations by{" "}
24
+ <span className="text-accent font-semibold">40%</span>, and I've shipped projects
25
+ like a BERT-based malicious URL detector (yes, nerdy - but cool :)).
26
+ </p>
27
+ <p className="text-navy font-semibold">
28
+ I believe in turning analytics into secure business strategy.
29
+ </p>
30
+ </div>
31
+ </div>
32
+ </section>
33
+ );
34
+ }
src/components/portfolio/sections/ContactSection.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CONTACT_EMAIL, RESUME_URL } from "@/lib/links";
2
+
3
+ export function ContactSection() {
4
+ return (
5
+ <section id="contact" className="py-24 bg-card/40 border-t border-border">
6
+ <div className="mx-auto max-w-5xl px-6 text-center">
7
+ <p className="font-display text-accent text-2xl tracking-widest">LET'S TALK</p>
8
+ <h2 className="mt-2 font-display text-5xl sm:text-6xl text-navy">
9
+ Let's build something <span className="text-accent">secure & smart</span>.
10
+ </h2>
11
+ <p className="mt-5 text-foreground/80 max-w-xl mx-auto">
12
+ Open to full-time roles in Business Analytics, Data Science, AI/ML
13
+ Engineering, and Network Security - based in Texas, open to relocation.
14
+ </p>
15
+
16
+ <div className="mt-8 flex flex-wrap justify-center gap-3">
17
+ <a
18
+ href={`mailto:${CONTACT_EMAIL}`}
19
+ className="inline-flex items-center rounded-full bg-navy px-6 py-3 text-base font-semibold text-primary-foreground hover:bg-accent transition-colors"
20
+ >
21
+ {CONTACT_EMAIL}
22
+ </a>
23
+ <a
24
+ href={RESUME_URL}
25
+ target="_blank"
26
+ rel="noreferrer"
27
+ className="inline-flex items-center rounded-full border border-navy/20 bg-card px-6 py-3 text-base font-semibold text-navy hover:border-accent hover:text-accent transition-colors"
28
+ >
29
+ Download Resume
30
+ </a>
31
+ </div>
32
+
33
+ <p className="mt-12 text-sm text-muted-foreground">
34
+ Dattebayo! Believe it, the right opportunity is one email away.
35
+ </p>
36
+ <p className="mt-3 text-xs text-muted-foreground">
37
+ (c) {new Date().getFullYear()} Manoj Guttikonda
38
+ </p>
39
+ </div>
40
+ </section>
41
+ );
42
+ }
src/components/portfolio/sections/CredentialsSection.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const certs = [
2
+ "Microsoft Certified: Azure Security Engineer Associate",
3
+ "Microsoft Azure Administrator - AZ-104",
4
+ "Microsoft Certified: Azure Fundamentals",
5
+ "Cybersecurity Foundation - Palo Alto Networks",
6
+ "IT Academy: Network Virtualization Concepts - VMware",
7
+ "AWS Academy Graduate - AWS Academy Cloud Foundations",
8
+ "Cloud Rider - 2020 - EduSkills Foundation",
9
+ "Data Science Foundation using R - 360DigiTMG",
10
+ "Machine Learning with Python - Cognitive Class",
11
+ "Database Management System - NPTEL",
12
+ "Introduction to Programming in C - NPTEL",
13
+ ];
14
+
15
+ const honors = [
16
+ "MSBA academic performance - 3.8 GPA at UNT",
17
+ "CyberSecurity Club, Business Analytics Club, and AIS member at UNT",
18
+ "Research work published at BIGS 2025 and SWDSI 2026",
19
+ ];
20
+
21
+ const leadership = [
22
+ "Vice President - College Photography Club",
23
+ "Indian Film Festival - 2018 (Qualified) & 2019",
24
+ "Multiple project seminars on Malicious URL Detection",
25
+ "Hackathon - Bennett University Internship Program",
26
+ ];
27
+
28
+ const campusExperience = [
29
+ {
30
+ role: "Instructional Assistant",
31
+ period: "Jan 2026 - May 2026",
32
+ details: "Supports course tasks, keeps class activities organized, helps students.",
33
+ },
34
+ {
35
+ role: "Library Student Assistant (Stacks)",
36
+ period: "Oct 2025 - Jan 2026",
37
+ details: "Managed shelving, records accuracy and daily workflow.",
38
+ },
39
+ {
40
+ role: "Clark Bakery Student Assistant",
41
+ period: "Nov 2024 - Oct 2025",
42
+ details: "Operations, customer service, inventory and food-safety standards.",
43
+ },
44
+ ];
45
+
46
+ export function CredentialsSection() {
47
+ return (
48
+ <section id="credentials" className="py-24 bg-card/40 border-y border-border">
49
+ <div className="mx-auto max-w-6xl px-6">
50
+ <p className="font-display text-accent text-2xl tracking-widest">CERTIFICATIONS & ACTIVITIES</p>
51
+ <h2 className="mt-2 font-display text-5xl text-navy">Credentials</h2>
52
+
53
+ <div className="mt-12 rounded-2xl border border-border bg-card p-6">
54
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
55
+ <div>
56
+ <h3 className="font-display text-xl text-navy">Certifications</h3>
57
+ <p className="mt-1 text-sm text-muted-foreground">
58
+ Cloud, cybersecurity, programming, and analytics credentials.
59
+ </p>
60
+ </div>
61
+ <span className="w-fit rounded-full bg-accent/10 px-3 py-1 text-xs font-semibold text-accent">
62
+ 11 credentials
63
+ </span>
64
+ </div>
65
+ <ul className="mt-5 grid gap-3 text-base text-foreground/85 sm:grid-cols-2 lg:grid-cols-3 tracking-wide">
66
+ {certs.map((c) => (
67
+ <li key={c} className="flex gap-2 rounded-xl border border-border/70 bg-background/60 px-3 py-2">
68
+ <span className="text-accent">&gt;</span>
69
+ <span>{c}</span>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+
75
+ <div className="mt-6 grid gap-6 lg:grid-cols-3">
76
+ <div className="rounded-2xl border border-border bg-card p-6">
77
+ <h3 className="font-display text-xl text-navy">Honors & Research</h3>
78
+ <ul className="mt-4 space-y-3 text-base text-foreground/85 tracking-wide leading-relaxed">
79
+ {honors.map((h) => (
80
+ <li key={h} className="flex gap-2">
81
+ <span className="text-accent">&gt;</span>
82
+ <span>{h}</span>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ </div>
87
+
88
+ <div className="rounded-2xl border border-border bg-card p-6 lg:col-span-2">
89
+ <h3 className="font-display text-xl text-navy">Leadership & Activities</h3>
90
+ <ul className="mt-4 grid gap-3 text-sm text-foreground/85 sm:grid-cols-2">
91
+ {leadership.map((l) => (
92
+ <li key={l} className="flex gap-2 rounded-xl border border-border/70 bg-background/60 px-3 py-2">
93
+ <span className="text-accent">&gt;</span>
94
+ <span>{l}</span>
95
+ </li>
96
+ ))}
97
+ </ul>
98
+ </div>
99
+ </div>
100
+
101
+ <div className="mt-6 rounded-2xl border border-border bg-card p-6">
102
+ <h3 className="font-display text-2xl text-navy">Campus Experience - UNT</h3>
103
+ <div className="mt-4 grid gap-5 text-base md:grid-cols-3">
104
+ {campusExperience.map((job) => (
105
+ <div key={job.role}>
106
+ <p className="font-semibold text-lg text-navy">{job.role}</p>
107
+ <p className="text-sm text-muted-foreground">{job.period}</p>
108
+ <p className="mt-1 text-base text-foreground/80">{job.details}</p>
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </section>
115
+ );
116
+ }
src/components/portfolio/sections/EducationSection.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const edu = [
2
+ {
3
+ school: "University of North Texas",
4
+ degree: "M.S. in Business Analytics",
5
+ period: "Aug 2024 - May 2026",
6
+ location: "Denton, TX",
7
+ gpa: "3.8 / 4.0",
8
+ },
9
+ {
10
+ school: "SRK Institute of Technology",
11
+ degree: "B.Tech, Computer Science Engineering",
12
+ period: "Jun 2017 - Aug 2021",
13
+ location: "India",
14
+ gpa: "3.5 / 4.0",
15
+ },
16
+ ];
17
+
18
+ const internships = [
19
+ {
20
+ company: "Gustovalley Technovations",
21
+ role: "Internship Trainee",
22
+ period: "Jul 2020",
23
+ location: "Andhra Pradesh, India",
24
+ points: [
25
+ "Completed an Industry 4.0 internship certification in online mode.",
26
+ "Attended workshops on cloud computing, AI, IoT, integrated systems, augmented reality, and Android mobility solutions.",
27
+ ],
28
+ },
29
+ {
30
+ company: "Bennett University",
31
+ role: "Intern",
32
+ period: "Dec 2019",
33
+ location: "Greater Noida, India",
34
+ points: [
35
+ "Completed an ML project on emotion recognition using EEG signals.",
36
+ "Attended AI and machine learning workshops and collaborated with participants across India.",
37
+ ],
38
+ },
39
+ ];
40
+
41
+ export function EducationSection() {
42
+ return (
43
+ <section id="education" className="py-24">
44
+ <div className="mx-auto max-w-6xl px-6">
45
+ <p className="font-display text-accent text-2xl tracking-widest">EDUCATION</p>
46
+ <h2 className="mt-2 font-display text-5xl text-navy">Education</h2>
47
+
48
+ <div className="mt-12 grid gap-6 lg:grid-cols-2">
49
+ {edu.map((e) => (
50
+ <div key={e.school} className="rounded-2xl border border-border bg-card p-6">
51
+ <p className="font-display text-2xl text-navy">{e.school}</p>
52
+ <p className="mt-2 text-base font-semibold text-foreground/85">{e.degree}</p>
53
+ <p className="mt-2 text-sm text-muted-foreground">
54
+ {e.period} - {e.location}
55
+ </p>
56
+ <p className="mt-4 inline-flex rounded-full bg-accent/10 px-3 py-1 text-xs font-semibold text-accent">
57
+ GPA: {e.gpa}
58
+ </p>
59
+ </div>
60
+ ))}
61
+ </div>
62
+
63
+ <div className="mt-10">
64
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
65
+ <div>
66
+ <h3 className="font-display text-3xl text-navy">Internships</h3>
67
+ <p className="mt-1 text-sm text-muted-foreground">
68
+ Early technical training in AI, Industry 4.0, and machine learning.
69
+ </p>
70
+ </div>
71
+ <span className="w-fit rounded-full bg-chakra/10 px-3 py-1 text-xs font-semibold text-chakra">
72
+ 2 internships
73
+ </span>
74
+ </div>
75
+
76
+ <div className="mt-5 grid gap-5 md:grid-cols-2">
77
+ {internships.map((internship) => (
78
+ <div key={internship.company} className="rounded-2xl border border-border bg-card p-5">
79
+ <div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
80
+ <div>
81
+ <p className="font-semibold text-navy">{internship.role}</p>
82
+ <p className="text-sm text-foreground/80">{internship.company}</p>
83
+ </div>
84
+ <p className="text-xs uppercase tracking-wider text-muted-foreground">{internship.period}</p>
85
+ </div>
86
+ <p className="mt-1 text-xs text-muted-foreground">{internship.location}</p>
87
+ <ul className="mt-4 space-y-2 text-base text-foreground/85 tracking-wide leading-relaxed">
88
+ {internship.points.map((point) => (
89
+ <li key={point} className="flex gap-2">
90
+ <span className="text-accent">&gt;</span>
91
+ <span>{point}</span>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </section>
101
+ );
102
+ }
src/components/portfolio/sections/FeaturedProjectsSection.tsx ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { ExternalLink, FileText, Github, Linkedin } from "lucide-react";
4
+ import { CONTACT_EMAIL, GITHUB_URL, HUGGING_FACE_URL, LINKEDIN_URL } from "@/lib/links";
5
+
6
+ type Project = {
7
+ id: string;
8
+ era: "UNT - Graduate" | "Accenture" | "Undergrad" | "Independent";
9
+ title: string;
10
+ tagline: string;
11
+ period: string;
12
+ details: string[];
13
+ stack: string[];
14
+ links?: {
15
+ label: string;
16
+ href: string;
17
+ }[];
18
+ };
19
+
20
+ const projects: Project[] = [
21
+ {
22
+ id: "alive-agents",
23
+ era: "Independent",
24
+ title: "Alive - AI Agent Orchestrator",
25
+ tagline: "Full-stack platform that assembles AI dev teams for any coding task.",
26
+ period: "2026",
27
+ details: [
28
+ "Spawns 5 role-based agents (Team Lead, Architect, FE Dev, BE Dev, Tester) per project.",
29
+ "Each agent runs on a different AI provider (DeepSeek, Qwen, Kimi, GLM, GPT) - all in parallel.",
30
+ "Isolated git worktrees per agent, real-time progress tracking, and live iframe preview.",
31
+ ],
32
+ stack: ["TypeScript", "Next.js", "Multi-Agent AI", "React", "Prisma"],
33
+ links: [
34
+ {
35
+ label: "View Code",
36
+ href: "https://github.com/im-mj/Agents-",
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ id: "nimbus-bank",
42
+ era: "Independent",
43
+ title: "Nimbus Bank - AI Triage System",
44
+ tagline: "LangGraph multi-agent pipeline for banking customer support.",
45
+ period: "Apr 2026",
46
+ details: [
47
+ "Built a LangGraph multi-agent system to classify and route banking support tickets.",
48
+ "Agents handle intent detection, policy lookup, escalation, and resolution - end to end.",
49
+ "Delivered full PRD, architecture diagram, and live Streamlit demo for Wipro pre-screening.",
50
+ ],
51
+ stack: ["Python", "LangGraph", "LLM", "Streamlit", "Multi-Agent"],
52
+ links: [
53
+ {
54
+ label: "View Code",
55
+ href: "https://github.com/im-mj/Nimbus-Bank-Triage",
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ id: "ghost-job-hunter",
61
+ era: "Independent",
62
+ title: "Ghost Job Hunter",
63
+ tagline: "Chrome extension concept for spotting suspicious or stale job posts.",
64
+ period: "2026",
65
+ details: [
66
+ "Flags ghost-job warning signs such as stale listings, repeated postings, vague role signals, and weak hiring intent.",
67
+ "Uses an AI-assisted scoring flow to help job seekers focus on real opportunities instead of wasting applications.",
68
+ "Connects analytics, recruiting pain points, and browser automation into a practical job-search tool.",
69
+ ],
70
+ stack: ["Chrome Extension", "JavaScript", "AI Scoring", "Job Analytics"],
71
+ links: [
72
+ {
73
+ label: "View Code",
74
+ href: "https://github.com/im-mj/Ghost-Job-Hunter",
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ id: "smartscreen",
80
+ era: "UNT - Graduate",
81
+ title: "SmartScreen - AI Resume Analyzer",
82
+ tagline: "AI-driven resume scoring & ATS-match feedback.",
83
+ period: "Jan 2025 - May 2025",
84
+ details: [
85
+ "Designed an AI scoring engine that grades resumes against a target JD and highlights ATS-killing gaps.",
86
+ "Generates targeted feedback so candidates can iterate quickly instead of guessing.",
87
+ "Built as an end-to-end concept: parsing -> scoring -> recommendations.",
88
+ ],
89
+ stack: ["Python", "NLP", "AI", "Data Analysis"],
90
+ },
91
+ {
92
+ id: "cvs",
93
+ era: "UNT - Graduate",
94
+ title: "CVS Pharmacy Strategy Thesis",
95
+ tagline: "Analytics-backed strategy across Finance, Marketing, Ops, HR & AI.",
96
+ period: "Jan 2025 - May 2025",
97
+ details: [
98
+ "Deep-dived 10-K filings and market data to build a cross-functional growth thesis.",
99
+ "Layered AI/automation recommendations on top of traditional strategy frames.",
100
+ "Delivered as a board-style presentation with prioritized initiatives.",
101
+ ],
102
+ stack: ["Research", "10-K Analysis", "Strategy", "Presentations"],
103
+ },
104
+ {
105
+ id: "jobs",
106
+ era: "UNT - Graduate",
107
+ title: "Job Market Analysis",
108
+ tagline: "Interactive Tableau dashboards on hiring trends & skills demand.",
109
+ period: "Aug 2024 - Dec 2024",
110
+ details: [
111
+ "Cleaned and modeled a multi-source job postings dataset.",
112
+ "Built dashboards that surface skill demand, salary bands, and role trajectories.",
113
+ "Designed for non-technical stakeholders to filter and self-serve.",
114
+ ],
115
+ stack: ["Tableau", "Data Viz", "SQL"],
116
+ },
117
+ {
118
+ id: "spend",
119
+ era: "UNT - Graduate",
120
+ title: "Student Spending Analysis",
121
+ tagline: "Regression + ANOVA on monthly student spend patterns.",
122
+ period: "Aug 2024 - Dec 2024",
123
+ details: [
124
+ "Collected and cleaned a primary dataset from student respondents.",
125
+ "Used regression and ANOVA to identify which factors actually moved spend.",
126
+ "Translated stats into plain-English recommendations.",
127
+ ],
128
+ stack: ["Minitab", "Excel", "Statistics"],
129
+ },
130
+ {
131
+ id: "smile",
132
+ era: "UNT - Graduate",
133
+ title: "Smile Buddy 3000 - Marketing Concept",
134
+ tagline: "Superhero-themed product campaign with positioning + messaging.",
135
+ period: "Jul 2025 - Sep 2025",
136
+ details: [
137
+ "Built persona, value prop, and a launch narrative around a fun mascot.",
138
+ "Designed campaign assets and a phased go-to-market plan.",
139
+ ],
140
+ stack: ["Marketing Strategy", "Brand", "Advertising"],
141
+ },
142
+ {
143
+ id: "secondlife",
144
+ era: "UNT - Graduate",
145
+ title: "Second Life - Organ Matching ML",
146
+ tagline: "ML pipeline for patient-organ matching and hospital triage.",
147
+ period: "2025",
148
+ details: [
149
+ "Built end-to-end ML pipeline with tiered matching, feature engineering, and model evaluation.",
150
+ "Modeled patient and hospital journeys; benchmarked classifiers for match scoring accuracy.",
151
+ "Deployed on HuggingFace Spaces with full architecture diagrams and a presentation deck.",
152
+ ],
153
+ stack: ["Python", "ML", "Flask", "SQL", "HuggingFace"],
154
+ links: [
155
+ {
156
+ label: "View Code",
157
+ href: "https://github.com/im-mj/SecondLife",
158
+ },
159
+ ],
160
+ },
161
+ {
162
+ id: "firewall-auto",
163
+ era: "Accenture",
164
+ title: "Firewall Rule Automation",
165
+ tagline: "Cut manual firewall-rule work by ~40% across enterprise estates.",
166
+ period: "2022 - 2024",
167
+ details: [
168
+ "Wrote scripts to push and validate firewall rule updates across multiple devices.",
169
+ "Reduced manual ticket cycle time and configuration drift.",
170
+ "Improved rule consistency across Telstra Global's hybrid environments.",
171
+ ],
172
+ stack: ["Python", "Shell", "Firewalls", "Automation"],
173
+ },
174
+ {
175
+ id: "malicious-url",
176
+ era: "Undergrad",
177
+ title: "Malicious URL Detection (ML)",
178
+ tagline: "Final-year ML project classifying URLs as safe vs malicious.",
179
+ period: "Nov 2020 - Feb 2021",
180
+ details: [
181
+ "Led a team during lockdown to build, train and evaluate the classifier.",
182
+ "Engineered URL features and benchmarked multiple ML models.",
183
+ "Presented results in multiple seminars to faculty and peers.",
184
+ ],
185
+ stack: ["Python", "Machine Learning", "Team Lead"],
186
+ links: [
187
+ {
188
+ label: "Research Details",
189
+ href: "#research-publications",
190
+ },
191
+ ],
192
+ },
193
+ {
194
+ id: "mask",
195
+ era: "Undergrad",
196
+ title: "COVID Face Mask Detection",
197
+ tagline: "Computer vision classifier on a custom dataset.",
198
+ period: "Mar 2020 - May 2020",
199
+ details: [
200
+ "Built a face-mask detector with a friend-collected custom dataset.",
201
+ "Iterated remotely during the early pandemic.",
202
+ ],
203
+ stack: ["Python", "ML", "Computer Vision"],
204
+ },
205
+ {
206
+ id: "eeg",
207
+ era: "Undergrad",
208
+ title: "Emotion Recognition from EEG Signals",
209
+ tagline: "Internship concept project applying ML to EEG.",
210
+ period: "Dec 2019",
211
+ details: [
212
+ "Explored ML approaches for classifying emotions from EEG signals.",
213
+ "Collaborated with multi-location team and a hackathon.",
214
+ ],
215
+ stack: ["Python", "ML", "Signal Processing"],
216
+ },
217
+ ];
218
+
219
+ const researchDetails = [
220
+ "Presented at BIGS 2025 and published for SWDSI 2026.",
221
+ "Built around AI-driven malicious URL detection using deep learning and BERT architecture.",
222
+ "Connects cybersecurity experience with machine learning research and business analytics storytelling.",
223
+ "Shows research writing, model evaluation, and technical presentation experience.",
224
+ ];
225
+
226
+ function HuggingFaceMark() {
227
+ return (
228
+ <span className="flex h-5 w-5 items-center justify-center rounded-md bg-amber-100 text-xs font-black text-amber-700">
229
+ HF
230
+ </span>
231
+ );
232
+ }
233
+
234
+ const workLinks = [
235
+ {
236
+ label: "LinkedIn",
237
+ href: LINKEDIN_URL,
238
+ note: "career profile",
239
+ icon: <Linkedin className="h-5 w-5" />,
240
+ },
241
+ {
242
+ label: "GitHub",
243
+ href: GITHUB_URL,
244
+ note: "code projects",
245
+ icon: <Github className="h-5 w-5" />,
246
+ },
247
+ {
248
+ label: "Hugging Face",
249
+ href: HUGGING_FACE_URL,
250
+ note: "AI work",
251
+ icon: <HuggingFaceMark />,
252
+ },
253
+ {
254
+ label: "Research Paper",
255
+ href: "#research-publications",
256
+ note: "BIGS & SWDSI",
257
+ icon: <FileText className="h-5 w-5" />,
258
+ },
259
+ ];
260
+
261
+ const eraColors: Record<Project["era"], string> = {
262
+ "UNT - Graduate": "bg-accent/15 text-accent border-accent/30",
263
+ Accenture: "bg-chakra/15 text-chakra border-chakra/30",
264
+ Undergrad: "bg-navy/10 text-navy border-navy/20",
265
+ Independent: "bg-emerald-600/10 text-emerald-700 border-emerald-600/25",
266
+ };
267
+
268
+ export function FeaturedProjectsSection() {
269
+ const [open, setOpen] = useState<string | null>(null);
270
+ const [researchOpen, setResearchOpen] = useState(false);
271
+
272
+ return (
273
+ <section id="projects" className="py-24">
274
+ <div className="mx-auto max-w-6xl px-6">
275
+ <div className="flex items-end justify-between flex-wrap gap-4">
276
+ <div>
277
+ <p className="font-display text-accent text-2xl tracking-widest">PROJECTS</p>
278
+ <h2 className="mt-2 font-display text-5xl text-navy">Featured Work</h2>
279
+ </div>
280
+ <p className="text-sm text-muted-foreground max-w-sm">
281
+ Hover or tap a card to read the story behind it.
282
+ </p>
283
+ </div>
284
+
285
+ <div className="mt-8 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
286
+ {workLinks.map((link) => (
287
+ <a
288
+ key={link.label}
289
+ href={link.href}
290
+ target={link.href.startsWith("#") ? undefined : "_blank"}
291
+ rel={link.href.startsWith("#") ? undefined : "noreferrer"}
292
+ className="group flex items-center justify-between gap-3 rounded-2xl border border-border bg-card px-4 py-4 text-left transition-all hover:border-accent/45 hover:bg-accent/10 hover:shadow-lg hover:shadow-accent/10"
293
+ >
294
+ <span className="flex items-center gap-3">
295
+ <span className="flex h-10 w-10 items-center justify-center rounded-xl border border-border bg-background text-navy transition-colors group-hover:border-accent/35 group-hover:text-accent">
296
+ {link.icon}
297
+ </span>
298
+ <span>
299
+ <span className="block font-display text-xl leading-none text-navy">{link.label}</span>
300
+ <span className="mt-1 block text-xs uppercase tracking-[0.14em] text-muted-foreground">
301
+ {link.note}
302
+ </span>
303
+ </span>
304
+ </span>
305
+ <ExternalLink className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-accent" />
306
+ </a>
307
+ ))}
308
+ </div>
309
+
310
+ <motion.div
311
+ id="research-publications"
312
+ role="button"
313
+ tabIndex={0}
314
+ onClick={() => setResearchOpen((current) => !current)}
315
+ onKeyDown={(event) => {
316
+ if (event.key === "Enter" || event.key === " ") {
317
+ event.preventDefault();
318
+ setResearchOpen((current) => !current);
319
+ }
320
+ }}
321
+ onMouseEnter={() => setResearchOpen(true)}
322
+ onMouseLeave={() => setResearchOpen(false)}
323
+ layout
324
+ className={`group mt-8 cursor-pointer rounded-2xl border bg-card px-6 py-5 shadow-sm transition-all ${
325
+ researchOpen ? "border-accent shadow-xl shadow-accent/10" : "border-accent/25 hover:border-accent/40"
326
+ }`}
327
+ >
328
+ <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
329
+ <div>
330
+ <p className="font-display text-2xl text-navy">Research Publications</p>
331
+ <p className="mt-1 text-sm text-foreground/75">
332
+ AI-Driven Malicious URL Detection using Deep Learning and BERT Architecture.
333
+ </p>
334
+ </div>
335
+ <span className="inline-flex w-fit rounded-full border border-accent/30 bg-card px-4 py-2 text-xs font-semibold uppercase tracking-[0.16em] text-accent">
336
+ Published at BIGS 2025 & SWDSI 2026
337
+ </span>
338
+ </div>
339
+
340
+ <AnimatePresence initial={false}>
341
+ {researchOpen && (
342
+ <motion.div
343
+ key="research-details"
344
+ initial={{ opacity: 0, height: 0 }}
345
+ animate={{ opacity: 1, height: "auto" }}
346
+ exit={{ opacity: 0, height: 0 }}
347
+ transition={{ duration: 0.25 }}
348
+ className="overflow-hidden"
349
+ >
350
+ <ul className="mt-5 grid gap-2 text-sm text-foreground/80 md:grid-cols-2 list-disc list-inside">
351
+ {researchDetails.map((detail) => (
352
+ <li key={detail}>{detail}</li>
353
+ ))}
354
+ </ul>
355
+ </motion.div>
356
+ )}
357
+ </AnimatePresence>
358
+ </motion.div>
359
+
360
+ <div className="mt-12 grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
361
+ {projects.map((p) => {
362
+ const isOpen = open === p.id;
363
+ const projectLinks =
364
+ p.links ?? [
365
+ {
366
+ label: "Request Details",
367
+ href: `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(`Project details: ${p.title}`)}`,
368
+ },
369
+ ];
370
+ return (
371
+ <motion.div
372
+ key={p.id}
373
+ role="button"
374
+ tabIndex={0}
375
+ onClick={() => setOpen(isOpen ? null : p.id)}
376
+ onKeyDown={(event) => {
377
+ if (event.key === "Enter" || event.key === " ") {
378
+ event.preventDefault();
379
+ setOpen(isOpen ? null : p.id);
380
+ }
381
+ }}
382
+ onMouseEnter={() => setOpen(p.id)}
383
+ onMouseLeave={() => setOpen(null)}
384
+ layout
385
+ className={`group relative text-left rounded-2xl border bg-card p-5 overflow-hidden transition-all ${
386
+ isOpen ? "border-accent shadow-xl shadow-accent/10" : "border-border hover:border-accent/40"
387
+ }`}
388
+ >
389
+ <div className="flex items-center justify-between gap-2">
390
+ <span className={`inline-block text-xs uppercase tracking-[0.18em] font-semibold rounded-full border px-2 py-0.5 ${eraColors[p.era]}`}>
391
+ {p.era}
392
+ </span>
393
+ <span className="text-xs text-muted-foreground">{p.period}</span>
394
+ </div>
395
+ <h3 className="mt-3 font-display text-xl text-navy leading-tight">{p.title}</h3>
396
+ <p className="mt-2 text-sm text-foreground/75 tracking-wide">{p.tagline}</p>
397
+
398
+ <AnimatePresence initial={false}>
399
+ {isOpen && (
400
+ <motion.div
401
+ key="details"
402
+ initial={{ opacity: 0, height: 0 }}
403
+ animate={{ opacity: 1, height: "auto" }}
404
+ exit={{ opacity: 0, height: 0 }}
405
+ transition={{ duration: 0.25 }}
406
+ className="overflow-hidden"
407
+ >
408
+ <ul className="mt-4 space-y-2 text-base text-foreground/80 list-disc list-inside tracking-wide leading-relaxed">
409
+ {p.details.map((d) => (
410
+ <li key={d}>{d}</li>
411
+ ))}
412
+ </ul>
413
+ <div className="mt-4 flex flex-wrap gap-2">
414
+ {projectLinks.map((link) => (
415
+ <a
416
+ key={link.label}
417
+ href={link.href}
418
+ target={link.href.startsWith("#") || link.href.startsWith("mailto:") ? undefined : "_blank"}
419
+ rel={link.href.startsWith("#") || link.href.startsWith("mailto:") ? undefined : "noreferrer"}
420
+ onClick={(event) => event.stopPropagation()}
421
+ className="inline-flex rounded-full border border-navy/20 px-4 py-2 text-xs font-semibold text-navy hover:border-accent hover:text-accent transition-colors"
422
+ >
423
+ {link.label}
424
+ </a>
425
+ ))}
426
+ </div>
427
+ </motion.div>
428
+ )}
429
+ </AnimatePresence>
430
+
431
+ <div className="mt-4 flex flex-wrap gap-1.5">
432
+ {p.stack.map((s) => (
433
+ <span key={s} className="rounded-md bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground tracking-wide">
434
+ {s}
435
+ </span>
436
+ ))}
437
+ </div>
438
+
439
+ <span className="absolute right-4 bottom-4 text-accent text-xs opacity-0 group-hover:opacity-100 transition-opacity">
440
+ {isOpen ? "-" : "+"}
441
+ </span>
442
+ </motion.div>
443
+ );
444
+ })}
445
+ </div>
446
+ </div>
447
+ </section>
448
+ );
449
+ }
src/components/portfolio/sections/HeroSection.tsx ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from "framer-motion";
2
+ import manojPhoto from "@/assets/manoj.jpg";
3
+ import { CONTACT_EMAIL } from "@/lib/links";
4
+
5
+ const roles = [
6
+ "Business Analytics",
7
+ "Data Analytics",
8
+ "Network Security",
9
+ "AI / ML Engineering",
10
+ "Data Science",
11
+ ];
12
+
13
+ export function HeroSection() {
14
+ return (
15
+ <section id="home" className="relative pt-28 pb-20 overflow-hidden">
16
+ <div className="absolute inset-0 bg-grid opacity-40 [mask-image:radial-gradient(ellipse_at_center,black,transparent_75%)]" />
17
+ <div className="relative mx-auto max-w-7xl px-6 lg:px-10 grid lg:grid-cols-[1.25fr_0.85fr] gap-12 xl:gap-16 items-start">
18
+ <div>
19
+ <motion.div
20
+ initial={{ opacity: 0, y: 10 }}
21
+ animate={{ opacity: 1, y: 0 }}
22
+ transition={{ duration: 0.5 }}
23
+ className="inline-flex items-center gap-2 rounded-full border border-accent/40 bg-accent/10 px-3 py-1 text-xs font-semibold text-accent uppercase tracking-wider"
24
+ >
25
+ <span className="h-2 w-2 rounded-full bg-accent animate-pulse" />
26
+ Open to work - Texas (open to relocation)
27
+ </motion.div>
28
+
29
+ <motion.h1
30
+ initial={{ opacity: 0, y: 16 }}
31
+ animate={{ opacity: 1, y: 0 }}
32
+ transition={{ duration: 0.6, delay: 0.05 }}
33
+ className="mt-5 font-display text-5xl sm:text-6xl lg:text-7xl leading-[0.95] tracking-[0.035em] text-navy"
34
+ >
35
+ MANOJ
36
+ <br />
37
+ <span className="text-accent">GUTTIKONDA</span>
38
+ </motion.h1>
39
+
40
+ <motion.p
41
+ initial={{ opacity: 0 }}
42
+ animate={{ opacity: 1 }}
43
+ transition={{ duration: 0.6, delay: 0.15 }}
44
+ className="mt-4 text-base sm:text-lg text-muted-foreground"
45
+ >
46
+ Business Analytics&nbsp;-&nbsp;Network Security&nbsp;-&nbsp;AI / ML
47
+ </motion.p>
48
+
49
+ <motion.p
50
+ initial={{ opacity: 0 }}
51
+ animate={{ opacity: 1 }}
52
+ transition={{ duration: 0.6, delay: 0.25 }}
53
+ className="mt-5 max-w-2xl text-base text-foreground/80 text-balance"
54
+ >
55
+ I make messy data behave and turn complicated systems into clear,
56
+ secure business decisions. <span className="text-accent font-semibold">Dattebayo!</span>
57
+ </motion.p>
58
+
59
+ <motion.div
60
+ initial={{ opacity: 0, y: 12 }}
61
+ animate={{ opacity: 1, y: 0 }}
62
+ transition={{ duration: 0.6, delay: 0.32 }}
63
+ className="mt-5 max-w-2xl border-l-2 border-accent/50 pl-4 space-y-3 text-sm sm:text-base leading-relaxed text-justify text-foreground/82"
64
+ >
65
+ <p>
66
+ I am an MS Business Analytics candidate at the University of North Texas
67
+ with a 3.8 GPA and nearly three years of cloud and network security
68
+ experience at Accenture. My work sits at the intersection of analytics,
69
+ automation, AI, and secure business operations.
70
+ </p>
71
+ <p>
72
+ At <span className="font-semibold text-navy">Accenture</span>, I worked with
73
+ enterprise firewalls, VPNs, load balancers, IPS tools, and cloud security
74
+ workflows. That experience taught me how complex systems operate in real
75
+ business environments and pushed me to use data to explain problems, measure
76
+ impact, and support better decisions.
77
+ </p>
78
+ <p>
79
+ I have built automation that reduced manual operations by{" "}
80
+ <span className="text-accent font-semibold">40%</span> and developed projects
81
+ in resume intelligence, market analysis, business strategy, and malicious URL
82
+ detection. I focus on turning messy data and technical complexity into clear,
83
+ secure, and practical business outcomes.
84
+ </p>
85
+ </motion.div>
86
+
87
+ <div className="mt-6 flex flex-wrap gap-2">
88
+ {roles.map((r) => (
89
+ <span
90
+ key={r}
91
+ className="rounded-full border border-border bg-card px-3 py-1 text-sm font-medium text-navy"
92
+ >
93
+ {r}
94
+ </span>
95
+ ))}
96
+ </div>
97
+
98
+ <div className="mt-7 flex flex-wrap gap-3">
99
+ <a
100
+ href="#projects"
101
+ className="inline-flex items-center rounded-full bg-navy px-6 py-3 text-sm font-semibold text-primary-foreground hover:bg-accent transition-colors"
102
+ >
103
+ See my work -&gt;
104
+ </a>
105
+ <a
106
+ href={`mailto:${CONTACT_EMAIL}`}
107
+ className="inline-flex items-center rounded-full border border-navy/20 bg-card px-6 py-3 text-sm font-semibold text-navy hover:border-accent hover:text-accent transition-colors"
108
+ >
109
+ Hire me
110
+ </a>
111
+ </div>
112
+
113
+ {/* Metric strip */}
114
+ <div className="mt-8 grid grid-cols-3 gap-3 max-w-xl">
115
+ {[
116
+ { k: "3.8", v: "MSBA GPA at University of North Texas" },
117
+ { k: "33 mo", v: "Cloud operations @ Accenture" },
118
+ { k: "2x", v: "Published AI research conferences" },
119
+ ].map((m) => (
120
+ <div key={m.k} className="rounded-xl border border-border bg-card/70 p-3">
121
+ <div className="font-display text-3xl text-accent">{m.k}</div>
122
+ <div className="mt-1 text-xs leading-tight text-muted-foreground">{m.v}</div>
123
+ </div>
124
+ ))}
125
+ </div>
126
+ </div>
127
+
128
+ <motion.div
129
+ initial={{ opacity: 0, scale: 0.92, y: 24 }}
130
+ animate={{ opacity: 1, scale: 1, y: 0 }}
131
+ transition={{ duration: 0.9, ease: "easeOut" }}
132
+ className="relative mx-auto lg:mr-0 w-full max-w-[460px] lg:max-w-[560px] lg:mt-24"
133
+ >
134
+ {/* colour-shifting glow */}
135
+ <motion.div
136
+ aria-hidden="true"
137
+ className="absolute -inset-10 rounded-[2rem] blur-3xl"
138
+ animate={{
139
+ background: [
140
+ "radial-gradient(ellipse at center, oklch(0.68 0.21 45 / 0.35), oklch(0.62 0.18 215 / 0.2), transparent)",
141
+ "radial-gradient(ellipse at center, oklch(0.62 0.18 215 / 0.35), oklch(0.22 0.075 265 / 0.2), transparent)",
142
+ "radial-gradient(ellipse at center, oklch(0.68 0.21 45 / 0.35), oklch(0.62 0.18 215 / 0.2), transparent)",
143
+ ],
144
+ scale: [0.95, 1.05, 0.95],
145
+ }}
146
+ transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
147
+ />
148
+
149
+ {/* floating orb — top right */}
150
+ <motion.div
151
+ aria-hidden="true"
152
+ className="absolute -right-6 top-8 h-20 w-20 rounded-full border border-accent/40 bg-accent/15"
153
+ animate={{ y: [0, -16, 0], x: [0, 5, 0], rotate: [0, 15, 0], scale: [1, 1.1, 1] }}
154
+ transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut" }}
155
+ />
156
+
157
+ {/* floating orb — bottom left */}
158
+ <motion.div
159
+ aria-hidden="true"
160
+ className="absolute -left-5 bottom-14 h-16 w-16 rounded-2xl border border-chakra/40 bg-chakra/15"
161
+ animate={{ y: [0, 12, 0], x: [0, -6, 0], rotate: [0, -12, 0], scale: [1, 0.9, 1] }}
162
+ transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
163
+ />
164
+
165
+ {/* floating dot — top left */}
166
+ <motion.div
167
+ aria-hidden="true"
168
+ className="absolute -left-3 top-24 h-8 w-8 rounded-full bg-accent/25"
169
+ animate={{ y: [0, -8, 0], opacity: [0.5, 1, 0.5] }}
170
+ transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
171
+ />
172
+
173
+ {/* floating dot — bottom right */}
174
+ <motion.div
175
+ aria-hidden="true"
176
+ className="absolute -right-3 bottom-32 h-6 w-6 rounded-full bg-chakra/30"
177
+ animate={{ y: [0, 8, 0], opacity: [0.4, 0.9, 0.4] }}
178
+ transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
179
+ />
180
+
181
+ {/* photo frame with 3D tilt */}
182
+ <motion.div
183
+ className="relative rounded-[2rem] shadow-2xl shadow-navy/20 [perspective:1200px]"
184
+ animate={{
185
+ y: [0, -12, 0],
186
+ rotateY: [-3, 4, -3],
187
+ rotateX: [2, -2, 2],
188
+ scale: [1, 1.012, 1],
189
+ }}
190
+ transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }}
191
+ >
192
+ <div className="relative overflow-hidden rounded-[1.55rem]">
193
+ <motion.img
194
+ src={manojPhoto}
195
+ alt="Manoj Guttikonda"
196
+ className="block aspect-[4/5] w-full object-cover object-center"
197
+ animate={{ scale: [1, 1.04, 1] }}
198
+ transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
199
+ />
200
+ {/* static colour overlay */}
201
+ <div className="pointer-events-none absolute inset-0 bg-gradient-to-tr from-navy/10 via-transparent to-accent/10" />
202
+ {/* shimmer sweep */}
203
+ <motion.div
204
+ aria-hidden="true"
205
+ className="pointer-events-none absolute inset-0"
206
+ style={{
207
+ background: "linear-gradient(120deg, transparent 30%, rgba(255,255,255,0.18) 50%, transparent 70%)",
208
+ backgroundSize: "200% 100%",
209
+ }}
210
+ animate={{ backgroundPosition: ["200% 0%", "-200% 0%"] }}
211
+ transition={{ duration: 3.5, repeat: Infinity, ease: "linear", repeatDelay: 2 }}
212
+ />
213
+ </div>
214
+ </motion.div>
215
+
216
+ {/* animated badge */}
217
+ <motion.div
218
+ className="absolute -bottom-5 right-4 rounded-2xl bg-navy text-primary-foreground px-4 py-2 text-xs font-semibold shadow-lg"
219
+ animate={{ y: [0, -5, 0], rotate: [-1, 1, -1] }}
220
+ transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
221
+ >
222
+ Believe it.
223
+ </motion.div>
224
+ </motion.div>
225
+ </div>
226
+ </section>
227
+ );
228
+ }
src/components/portfolio/sections/JourneySection.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ const items = [
4
+ {
5
+ tag: "UNDERGRADUATION",
6
+ title: "B.Tech, Computer Science Engineering",
7
+ org: "SRK Institute of Technology - India",
8
+ period: "Jun 2017 - Aug 2021",
9
+ points: [
10
+ "Built a programming and problem-solving foundation.",
11
+ "Explored machine learning through a malicious URL detection project.",
12
+ "Served as Vice President of the Photography Club.",
13
+ ],
14
+ logo: "undergrad",
15
+ },
16
+ {
17
+ tag: "WORK EXPERIENCE",
18
+ title: "Cloud Operations Engineer - Telstra Global",
19
+ org: "Accenture - Bengaluru, India",
20
+ period: "Oct 2021 - Jun 2024 (33 months)",
21
+ points: [
22
+ "Supported Telstra Global's enterprise cloud infrastructure across AWS and Azure environments.",
23
+ "Administered firewalls, VPNs, load balancers, and IPS to support 99%+ uptime and policy compliance.",
24
+ "Monitored and triaged network incidents through structured incident management workflows.",
25
+ "Automated operational processes with scripting to reduce manual workloads.",
26
+ ],
27
+ logo: "accenture",
28
+ },
29
+ {
30
+ tag: "GRADUATION",
31
+ title: "Master's Degree, Business Analytics",
32
+ org: "University of North Texas - Denton, TX",
33
+ period: "Aug 2024 - May 2026",
34
+ points: [
35
+ "Maintaining a 3.8 GPA in the MSBA program.",
36
+ "Built AI and analytics projects including Ghost Job Hunter and SmartScreen.",
37
+ "Published AI-driven malicious URL detection research at BIGS 2025 and SWDSI 2026.",
38
+ ],
39
+ logo: "unt",
40
+ },
41
+ ];
42
+
43
+ const circleDelays = [0, 1200, 2400];
44
+ const textDelays = [420, 1620, 2820];
45
+ const lineDelays = [650, 1850];
46
+
47
+ function JourneyLogo({ logo }: { logo: string }) {
48
+ if (logo === "undergrad") {
49
+ return (
50
+ <svg
51
+ viewBox="0 0 180 112"
52
+ className="h-20 w-24"
53
+ role="img"
54
+ aria-label="SRK Institute of Technology"
55
+ >
56
+ <path
57
+ d="M15 65h150v34H15z"
58
+ fill="#2f2a83"
59
+ />
60
+ <path
61
+ d="M24 64a66 66 0 0 1 132 0h-18a48 48 0 0 0-96 0z"
62
+ fill="#302c87"
63
+ />
64
+ <path
65
+ d="M90 24v12M64 34l6 10M116 34l-6 10M48 54l12 3M132 54l-12 3"
66
+ stroke="#f15b2a"
67
+ strokeWidth="5"
68
+ strokeLinecap="round"
69
+ />
70
+ <path
71
+ d="M43 65a47 47 0 0 1 94 0"
72
+ fill="none"
73
+ stroke="#ffffff"
74
+ strokeWidth="7"
75
+ />
76
+ <text x="90" y="84" textAnchor="middle" fontSize="20" fontWeight="800" fill="#f15b2a">
77
+ SRK
78
+ </text>
79
+ <text x="90" y="101" textAnchor="middle" fontSize="12" fontWeight="700" fill="#ffffff">
80
+ VIJAYAWADA
81
+ </text>
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ if (logo === "accenture") {
87
+ return <span className="font-display text-8xl leading-none text-[#8d3ff2]">&gt;</span>;
88
+ }
89
+
90
+ return <span className="font-serif text-5xl font-bold tracking-tight text-[#3d8b3d]">UNT</span>;
91
+ }
92
+
93
+ export function JourneySection() {
94
+ const sectionRef = useRef<HTMLElement | null>(null);
95
+ const [hasEntered, setHasEntered] = useState(false);
96
+
97
+ useEffect(() => {
98
+ const section = sectionRef.current;
99
+ if (!section) return;
100
+
101
+ const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
102
+ if (reduceMotion) {
103
+ setHasEntered(true);
104
+ return;
105
+ }
106
+
107
+ const observer = new IntersectionObserver(
108
+ ([entry]) => {
109
+ if (entry.isIntersecting) {
110
+ setHasEntered(true);
111
+ observer.disconnect();
112
+ }
113
+ },
114
+ { threshold: 0.35 },
115
+ );
116
+
117
+ observer.observe(section);
118
+ return () => observer.disconnect();
119
+ }, []);
120
+
121
+ return (
122
+ <section ref={sectionRef} id="journey" className="pt-16 pb-8 bg-card/40 border-y border-border">
123
+ <div className="mx-auto max-w-6xl px-6">
124
+ <div>
125
+ <p className="font-display text-accent text-2xl tracking-widest">JOURNEY</p>
126
+ <h2 className="mt-2 font-display text-5xl text-navy">My Journey</h2>
127
+ </div>
128
+
129
+ <div className="relative mt-12">
130
+ {[0, 1].map((lineIndex) => (
131
+ <div
132
+ key={lineIndex}
133
+ className="absolute top-[88px] hidden h-px overflow-hidden md:block"
134
+ style={{
135
+ left: lineIndex === 0 ? "calc(16.666% + 56px)" : "calc(50% + 56px)",
136
+ width: "calc(33.333% - 112px)",
137
+ }}
138
+ >
139
+ <div
140
+ className={`h-full origin-left bg-navy/45 transition-transform duration-700 ease-out ${
141
+ hasEntered ? "scale-x-100" : "scale-x-0"
142
+ }`}
143
+ style={{ transitionDelay: hasEntered ? `${lineDelays[lineIndex]}ms` : "0ms" }}
144
+ />
145
+ </div>
146
+ ))}
147
+ <div className="grid gap-12 md:grid-cols-3 md:gap-10">
148
+
149
+ {items.map((it, index) => (
150
+ <div
151
+ key={it.tag}
152
+ className="group relative z-10 min-h-[320px] text-center"
153
+ >
154
+ <p className="font-display text-lg tracking-[0.25em] text-accent">{it.tag}</p>
155
+
156
+ <div
157
+ className={`relative z-10 mx-auto mt-4 flex h-28 w-28 items-center justify-center rounded-full border-4 border-background bg-card shadow-lg shadow-navy/10 ring-1 ring-border transition-all duration-700 ease-out group-hover:-translate-y-1 group-hover:scale-105 group-focus-within:-translate-y-1 group-focus-within:scale-105 ${
158
+ hasEntered ? "translate-x-0 opacity-100" : "-translate-x-40 opacity-0"
159
+ }`}
160
+ style={{ transitionDelay: hasEntered ? `${circleDelays[index]}ms` : "0ms" }}
161
+ >
162
+ <button
163
+ type="button"
164
+ className="flex h-full w-full items-center justify-center rounded-full focus:outline-none focus:ring-4 focus:ring-accent/25"
165
+ aria-label={`${it.tag} details`}
166
+ >
167
+ <JourneyLogo logo={it.logo} />
168
+ </button>
169
+ </div>
170
+
171
+ <div
172
+ className={`mt-7 min-h-[92px] transition-all duration-500 ease-out ${
173
+ hasEntered ? "translate-y-0 opacity-100" : "translate-y-3 opacity-0"
174
+ }`}
175
+ style={{ transitionDelay: hasEntered ? `${textDelays[index]}ms` : "0ms" }}
176
+ >
177
+ <h3 className="font-display text-xl text-navy leading-snug tracking-wide">{it.title}</h3>
178
+ <p className="mt-3 text-sm font-medium text-muted-foreground">{it.org}</p>
179
+ <p className="mt-2 text-xs uppercase tracking-wider text-foreground/60">{it.period}</p>
180
+ </div>
181
+
182
+ <div className="pointer-events-none absolute left-1/2 top-[318px] z-20 w-[320px] -translate-x-1/2 translate-y-2 rounded-xl border border-border bg-card p-4 text-left text-sm leading-relaxed text-foreground/80 opacity-0 shadow-xl shadow-navy/10 transition-all duration-200 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:translate-y-0 group-focus-within:opacity-100">
183
+ <ul className="list-disc space-y-2 pl-5">
184
+ {it.points.map((point) => (
185
+ <li key={point}>{point}</li>
186
+ ))}
187
+ </ul>
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </section>
195
+ );
196
+ }
src/components/portfolio/sections/SkillsSection.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const groups = [
2
+ {
3
+ title: "Analytics & Data",
4
+ items: ["SQL", "Python", "Pandas", "Excel", "Minitab", "Tableau", "Power BI", "Business Analytics", "Statistics", "Regression / ANOVA"],
5
+ },
6
+ {
7
+ title: "AI / ML",
8
+ items: ["Artificial Intelligence", "Machine Learning", "NLP", "BERT", "TensorFlow", "LLMs", "Computer Vision", "Scikit-learn", "AI Strategy"],
9
+ },
10
+ {
11
+ title: "Security & Cloud",
12
+ items: ["AWS", "Azure", "Microsoft AZ-104", "Microsoft AZ-500", "Firewalls", "VPNs", "Load Balancers", "IPS", "Cloud Operations"],
13
+ },
14
+ {
15
+ title: "Tools & Workflow",
16
+ items: ["Git", "Linux", "Shell Scripting", "Automation", "Jira", "ServiceNow", "Presentations"],
17
+ },
18
+ ];
19
+
20
+ export function SkillsSection() {
21
+ return (
22
+ <section id="skills" className="py-24 bg-card/40 border-y border-border">
23
+ <div className="mx-auto max-w-7xl px-6">
24
+ <p className="font-display text-accent text-2xl tracking-widest">TOOLKIT</p>
25
+ <h2 className="mt-2 font-display text-5xl text-navy">Skills</h2>
26
+
27
+ <div className="mt-12 grid md:grid-cols-2 lg:grid-cols-4 gap-5">
28
+ {groups.map((g) => (
29
+ <div key={g.title} className="rounded-2xl border border-border bg-card p-5">
30
+ <h3 className="font-display text-lg text-navy">{g.title}</h3>
31
+ <ul className="mt-3 flex flex-wrap gap-1.5">
32
+ {g.items.map((i) => (
33
+ <li
34
+ key={i}
35
+ className="rounded-md border border-border bg-background px-2.5 py-1 text-base text-foreground/80"
36
+ >
37
+ {i}
38
+ </li>
39
+ ))}
40
+ </ul>
41
+ </div>
42
+ ))}
43
+ </div>
44
+ </div>
45
+ </section>
46
+ );
47
+ }
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Accordion = AccordionPrimitive.Root;
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
14
+ ));
15
+ AccordionItem.displayName = "AccordionItem";
16
+
17
+ const AccordionTrigger = React.forwardRef<
18
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
19
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
20
+ >(({ className, children, ...props }, ref) => (
21
+ <AccordionPrimitive.Header className="flex">
22
+ <AccordionPrimitive.Trigger
23
+ ref={ref}
24
+ className={cn(
25
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium cursor-pointer transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
32
+ </AccordionPrimitive.Trigger>
33
+ </AccordionPrimitive.Header>
34
+ ));
35
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
36
+
37
+ const AccordionContent = React.forwardRef<
38
+ React.ElementRef<typeof AccordionPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <AccordionPrimitive.Content
42
+ ref={ref}
43
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
44
+ {...props}
45
+ >
46
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
47
+ </AccordionPrimitive.Content>
48
+ ));
49
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
50
+
51
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "@/components/ui/button";
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ));
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
+ );
49
+ AlertDialogHeader.displayName = "AlertDialogHeader";
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
54
+ {...props}
55
+ />
56
+ );
57
+ AlertDialogFooter.displayName = "AlertDialogFooter";
58
+
59
+ const AlertDialogTitle = React.forwardRef<
60
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
61
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
62
+ >(({ className, ...props }, ref) => (
63
+ <AlertDialogPrimitive.Title
64
+ ref={ref}
65
+ className={cn("text-lg font-semibold", className)}
66
+ {...props}
67
+ />
68
+ ));
69
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
70
+
71
+ const AlertDialogDescription = React.forwardRef<
72
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
73
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
74
+ >(({ className, ...props }, ref) => (
75
+ <AlertDialogPrimitive.Description
76
+ ref={ref}
77
+ className={cn("text-sm text-muted-foreground", className)}
78
+ {...props}
79
+ />
80
+ ));
81
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
82
+
83
+ const AlertDialogAction = React.forwardRef<
84
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
85
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
86
+ >(({ className, ...props }, ref) => (
87
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
88
+ ));
89
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
90
+
91
+ const AlertDialogCancel = React.forwardRef<
92
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
93
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
94
+ >(({ className, ...props }, ref) => (
95
+ <AlertDialogPrimitive.Cancel
96
+ ref={ref}
97
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
98
+ {...props}
99
+ />
100
+ ));
101
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
102
+
103
+ export {
104
+ AlertDialog,
105
+ AlertDialogPortal,
106
+ AlertDialogOverlay,
107
+ AlertDialogTrigger,
108
+ AlertDialogContent,
109
+ AlertDialogHeader,
110
+ AlertDialogFooter,
111
+ AlertDialogTitle,
112
+ AlertDialogDescription,
113
+ AlertDialogAction,
114
+ AlertDialogCancel,
115
+ };
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
27
+ ));
28
+ Alert.displayName = "Alert";
29
+
30
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
31
+ ({ className, ...props }, ref) => (
32
+ <h5
33
+ ref={ref}
34
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
35
+ {...props}
36
+ />
37
+ ),
38
+ );
39
+ AlertTitle.displayName = "AlertTitle";
40
+
41
+ const AlertDescription = React.forwardRef<
42
+ HTMLParagraphElement,
43
+ React.HTMLAttributes<HTMLParagraphElement>
44
+ >(({ className, ...props }, ref) => (
45
+ <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
46
+ ));
47
+ AlertDescription.displayName = "AlertDescription";
48
+
49
+ export { Alert, AlertTitle, AlertDescription };
src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root;
4
+
5
+ export { AspectRatio };
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
15
+ {...props}
16
+ />
17
+ ));
18
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
19
+
20
+ const AvatarImage = React.forwardRef<
21
+ React.ElementRef<typeof AvatarPrimitive.Image>,
22
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
23
+ >(({ className, ...props }, ref) => (
24
+ <AvatarPrimitive.Image
25
+ ref={ref}
26
+ className={cn("aspect-square h-full w-full", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31
+
32
+ const AvatarFallback = React.forwardRef<
33
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
34
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
35
+ >(({ className, ...props }, ref) => (
36
+ <AvatarPrimitive.Fallback
37
+ ref={ref}
38
+ className={cn(
39
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46
+
47
+ export { Avatar, AvatarImage, AvatarFallback };
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ },
23
+ );
24
+
25
+ export interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
27
+
28
+ function Badge({ className, variant, ...props }: BadgeProps) {
29
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
+ }
31
+
32
+ export { Badge, badgeVariants };
src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode;
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13
+ Breadcrumb.displayName = "Breadcrumb";
14
+
15
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
16
+ ({ className, ...props }, ref) => (
17
+ <ol
18
+ ref={ref}
19
+ className={cn(
20
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ ),
26
+ );
27
+ BreadcrumbList.displayName = "BreadcrumbList";
28
+
29
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
30
+ ({ className, ...props }, ref) => (
31
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
32
+ ),
33
+ );
34
+ BreadcrumbItem.displayName = "BreadcrumbItem";
35
+
36
+ const BreadcrumbLink = React.forwardRef<
37
+ HTMLAnchorElement,
38
+ React.ComponentPropsWithoutRef<"a"> & {
39
+ asChild?: boolean;
40
+ }
41
+ >(({ asChild, className, ...props }, ref) => {
42
+ const Comp = asChild ? Slot : "a";
43
+
44
+ return (
45
+ <Comp
46
+ ref={ref}
47
+ className={cn("transition-colors hover:text-foreground", className)}
48
+ {...props}
49
+ />
50
+ );
51
+ });
52
+ BreadcrumbLink.displayName = "BreadcrumbLink";
53
+
54
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
55
+ ({ className, ...props }, ref) => (
56
+ <span
57
+ ref={ref}
58
+ role="link"
59
+ aria-disabled="true"
60
+ aria-current="page"
61
+ className={cn("font-normal text-foreground", className)}
62
+ {...props}
63
+ />
64
+ ),
65
+ );
66
+ BreadcrumbPage.displayName = "BreadcrumbPage";
67
+
68
+ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
69
+ <li
70
+ role="presentation"
71
+ aria-hidden="true"
72
+ className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
73
+ {...props}
74
+ >
75
+ {children ?? <ChevronRight />}
76
+ </li>
77
+ );
78
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
79
+
80
+ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
81
+ <span
82
+ role="presentation"
83
+ aria-hidden="true"
84
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
85
+ {...props}
86
+ >
87
+ <MoreHorizontal className="h-4 w-4" />
88
+ <span className="sr-only">More</span>
89
+ </span>
90
+ );
91
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
92
+
93
+ export {
94
+ Breadcrumb,
95
+ BreadcrumbList,
96
+ BreadcrumbItem,
97
+ BreadcrumbLink,
98
+ BreadcrumbPage,
99
+ BreadcrumbSeparator,
100
+ BreadcrumbEllipsis,
101
+ };
src/components/ui/button.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
14
+ outline:
15
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
16
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
17
+ ghost: "hover:bg-accent hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default: "h-9 px-4 py-2",
22
+ sm: "h-8 rounded-md px-3 text-xs",
23
+ lg: "h-10 rounded-md px-8",
24
+ icon: "h-9 w-9",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ variant: "default",
29
+ size: "default",
30
+ },
31
+ },
32
+ );
33
+
34
+ export interface ButtonProps
35
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : "button";
42
+ return (
43
+ <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
+ );
45
+ },
46
+ );
47
+ Button.displayName = "Button";
48
+
49
+ export { Button, buttonVariants };
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { Button, buttonVariants } from "@/components/ui/button";
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ captionLayout = "label",
15
+ buttonVariant = "ghost",
16
+ formatters,
17
+ components,
18
+ ...props
19
+ }: React.ComponentProps<typeof DayPicker> & {
20
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
21
+ }) {
22
+ const defaultClassNames = getDefaultClassNames();
23
+
24
+ return (
25
+ <DayPicker
26
+ showOutsideDays={showOutsideDays}
27
+ className={cn(
28
+ "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
29
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
30
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
31
+ className,
32
+ )}
33
+ captionLayout={captionLayout}
34
+ formatters={{
35
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
36
+ ...formatters,
37
+ }}
38
+ classNames={{
39
+ root: cn("w-fit", defaultClassNames.root),
40
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
41
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
42
+ nav: cn(
43
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
44
+ defaultClassNames.nav,
45
+ ),
46
+ button_previous: cn(
47
+ buttonVariants({ variant: buttonVariant }),
48
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
49
+ defaultClassNames.button_previous,
50
+ ),
51
+ button_next: cn(
52
+ buttonVariants({ variant: buttonVariant }),
53
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
54
+ defaultClassNames.button_next,
55
+ ),
56
+ month_caption: cn(
57
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
58
+ defaultClassNames.month_caption,
59
+ ),
60
+ dropdowns: cn(
61
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
62
+ defaultClassNames.dropdowns,
63
+ ),
64
+ dropdown_root: cn(
65
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
66
+ defaultClassNames.dropdown_root,
67
+ ),
68
+ dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
69
+ caption_label: cn(
70
+ "select-none font-medium",
71
+ captionLayout === "label"
72
+ ? "text-sm"
73
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
74
+ defaultClassNames.caption_label,
75
+ ),
76
+ table: "w-full border-collapse",
77
+ weekdays: cn("flex", defaultClassNames.weekdays),
78
+ weekday: cn(
79
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
80
+ defaultClassNames.weekday,
81
+ ),
82
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
83
+ week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
84
+ week_number: cn(
85
+ "text-muted-foreground select-none text-[0.8rem]",
86
+ defaultClassNames.week_number,
87
+ ),
88
+ day: cn(
89
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
90
+ defaultClassNames.day,
91
+ ),
92
+ range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
93
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
94
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
95
+ today: cn(
96
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
97
+ defaultClassNames.today,
98
+ ),
99
+ outside: cn(
100
+ "text-muted-foreground aria-selected:text-muted-foreground",
101
+ defaultClassNames.outside,
102
+ ),
103
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
104
+ hidden: cn("invisible", defaultClassNames.hidden),
105
+ ...classNames,
106
+ }}
107
+ components={{
108
+ Root: ({ className, rootRef, ...props }) => {
109
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
110
+ },
111
+ Chevron: ({ className, orientation, ...props }) => {
112
+ if (orientation === "left") {
113
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
114
+ }
115
+
116
+ if (orientation === "right") {
117
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
118
+ }
119
+
120
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
121
+ },
122
+ DayButton: CalendarDayButton,
123
+ WeekNumber: ({ children, ...props }) => {
124
+ return (
125
+ <td {...props}>
126
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
127
+ {children}
128
+ </div>
129
+ </td>
130
+ );
131
+ },
132
+ ...components,
133
+ }}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function CalendarDayButton({
140
+ className,
141
+ day,
142
+ modifiers,
143
+ ...props
144
+ }: React.ComponentProps<typeof DayButton>) {
145
+ const defaultClassNames = getDefaultClassNames();
146
+
147
+ const ref = React.useRef<HTMLButtonElement>(null);
148
+ React.useEffect(() => {
149
+ if (modifiers.focused) ref.current?.focus();
150
+ }, [modifiers.focused]);
151
+
152
+ return (
153
+ <Button
154
+ ref={ref}
155
+ variant="ghost"
156
+ size="icon"
157
+ data-day={day.date.toLocaleDateString()}
158
+ data-selected-single={
159
+ modifiers.selected &&
160
+ !modifiers.range_start &&
161
+ !modifiers.range_end &&
162
+ !modifiers.range_middle
163
+ }
164
+ data-range-start={modifiers.range_start}
165
+ data-range-end={modifiers.range_end}
166
+ data-range-middle={modifiers.range_middle}
167
+ className={cn(
168
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
169
+ defaultClassNames.day,
170
+ className,
171
+ )}
172
+ {...props}
173
+ />
174
+ );
175
+ }
176
+
177
+ export { Calendar, CalendarDayButton };
src/components/ui/card.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
10
+ {...props}
11
+ />
12
+ ),
13
+ );
14
+ Card.displayName = "Card";
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
19
+ ),
20
+ );
21
+ CardHeader.displayName = "CardHeader";
22
+
23
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn("font-semibold leading-none tracking-tight", className)}
28
+ {...props}
29
+ />
30
+ ),
31
+ );
32
+ CardTitle.displayName = "CardTitle";
33
+
34
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
37
+ ),
38
+ );
39
+ CardDescription.displayName = "CardDescription";
40
+
41
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
+ ({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
44
+ ),
45
+ );
46
+ CardContent.displayName = "CardContent";
47
+
48
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
51
+ ),
52
+ );
53
+ CardFooter.displayName = "CardFooter";
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
3
+ import { ArrowLeft, ArrowRight } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ type CarouselApi = UseEmblaCarouselType[1];
9
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
10
+ type CarouselOptions = UseCarouselParameters[0];
11
+ type CarouselPlugin = UseCarouselParameters[1];
12
+
13
+ type CarouselProps = {
14
+ opts?: CarouselOptions;
15
+ plugins?: CarouselPlugin;
16
+ orientation?: "horizontal" | "vertical";
17
+ setApi?: (api: CarouselApi) => void;
18
+ };
19
+
20
+ type CarouselContextProps = {
21
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
22
+ api: ReturnType<typeof useEmblaCarousel>[1];
23
+ scrollPrev: () => void;
24
+ scrollNext: () => void;
25
+ canScrollPrev: boolean;
26
+ canScrollNext: boolean;
27
+ } & CarouselProps;
28
+
29
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
30
+
31
+ function useCarousel() {
32
+ const context = React.useContext(CarouselContext);
33
+
34
+ if (!context) {
35
+ throw new Error("useCarousel must be used within a <Carousel />");
36
+ }
37
+
38
+ return context;
39
+ }
40
+
41
+ const Carousel = React.forwardRef<
42
+ HTMLDivElement,
43
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
44
+ >(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
45
+ const [carouselRef, api] = useEmblaCarousel(
46
+ {
47
+ ...opts,
48
+ axis: orientation === "horizontal" ? "x" : "y",
49
+ },
50
+ plugins,
51
+ );
52
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
53
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
54
+
55
+ const onSelect = React.useCallback((api: CarouselApi) => {
56
+ if (!api) {
57
+ return;
58
+ }
59
+
60
+ setCanScrollPrev(api.canScrollPrev());
61
+ setCanScrollNext(api.canScrollNext());
62
+ }, []);
63
+
64
+ const scrollPrev = React.useCallback(() => {
65
+ api?.scrollPrev();
66
+ }, [api]);
67
+
68
+ const scrollNext = React.useCallback(() => {
69
+ api?.scrollNext();
70
+ }, [api]);
71
+
72
+ const handleKeyDown = React.useCallback(
73
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
74
+ if (event.key === "ArrowLeft") {
75
+ event.preventDefault();
76
+ scrollPrev();
77
+ } else if (event.key === "ArrowRight") {
78
+ event.preventDefault();
79
+ scrollNext();
80
+ }
81
+ },
82
+ [scrollPrev, scrollNext],
83
+ );
84
+
85
+ React.useEffect(() => {
86
+ if (!api || !setApi) {
87
+ return;
88
+ }
89
+
90
+ setApi(api);
91
+ }, [api, setApi]);
92
+
93
+ React.useEffect(() => {
94
+ if (!api) {
95
+ return;
96
+ }
97
+
98
+ onSelect(api);
99
+ api.on("reInit", onSelect);
100
+ api.on("select", onSelect);
101
+
102
+ return () => {
103
+ api?.off("select", onSelect);
104
+ };
105
+ }, [api, onSelect]);
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
114
+ scrollPrev,
115
+ scrollNext,
116
+ canScrollPrev,
117
+ canScrollNext,
118
+ }}
119
+ >
120
+ <div
121
+ ref={ref}
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ {...props}
127
+ >
128
+ {children}
129
+ </div>
130
+ </CarouselContext.Provider>
131
+ );
132
+ });
133
+ Carousel.displayName = "Carousel";
134
+
135
+ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
+ ({ className, ...props }, ref) => {
137
+ const { carouselRef, orientation } = useCarousel();
138
+
139
+ return (
140
+ <div ref={carouselRef} className="overflow-hidden">
141
+ <div
142
+ ref={ref}
143
+ className={cn(
144
+ "flex",
145
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
146
+ className,
147
+ )}
148
+ {...props}
149
+ />
150
+ </div>
151
+ );
152
+ },
153
+ );
154
+ CarouselContent.displayName = "CarouselContent";
155
+
156
+ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
157
+ ({ className, ...props }, ref) => {
158
+ const { orientation } = useCarousel();
159
+
160
+ return (
161
+ <div
162
+ ref={ref}
163
+ role="group"
164
+ aria-roledescription="slide"
165
+ className={cn(
166
+ "min-w-0 shrink-0 grow-0 basis-full",
167
+ orientation === "horizontal" ? "pl-4" : "pt-4",
168
+ className,
169
+ )}
170
+ {...props}
171
+ />
172
+ );
173
+ },
174
+ );
175
+ CarouselItem.displayName = "CarouselItem";
176
+
177
+ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
178
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
179
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
180
+
181
+ return (
182
+ <Button
183
+ ref={ref}
184
+ variant={variant}
185
+ size={size}
186
+ className={cn(
187
+ "absolute h-8 w-8 rounded-full",
188
+ orientation === "horizontal"
189
+ ? "-left-12 top-1/2 -translate-y-1/2"
190
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
191
+ className,
192
+ )}
193
+ disabled={!canScrollPrev}
194
+ onClick={scrollPrev}
195
+ {...props}
196
+ >
197
+ <ArrowLeft className="h-4 w-4" />
198
+ <span className="sr-only">Previous slide</span>
199
+ </Button>
200
+ );
201
+ },
202
+ );
203
+ CarouselPrevious.displayName = "CarouselPrevious";
204
+
205
+ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
206
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
207
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
208
+
209
+ return (
210
+ <Button
211
+ ref={ref}
212
+ variant={variant}
213
+ size={size}
214
+ className={cn(
215
+ "absolute h-8 w-8 rounded-full",
216
+ orientation === "horizontal"
217
+ ? "-right-12 top-1/2 -translate-y-1/2"
218
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
219
+ className,
220
+ )}
221
+ disabled={!canScrollNext}
222
+ onClick={scrollNext}
223
+ {...props}
224
+ >
225
+ <ArrowRight className="h-4 w-4" />
226
+ <span className="sr-only">Next slide</span>
227
+ </Button>
228
+ );
229
+ },
230
+ );
231
+ CarouselNext.displayName = "CarouselNext";
232
+
233
+ export {
234
+ type CarouselApi,
235
+ Carousel,
236
+ CarouselContent,
237
+ CarouselItem,
238
+ CarouselPrevious,
239
+ CarouselNext,
240
+ };
src/components/ui/chart.tsx ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as RechartsPrimitive from "recharts";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode;
12
+ icon?: React.ComponentType;
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ );
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig;
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext);
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />");
30
+ }
31
+
32
+ return context;
33
+ }
34
+
35
+ const ChartContainer = React.forwardRef<
36
+ HTMLDivElement,
37
+ React.ComponentProps<"div"> & {
38
+ config: ChartConfig;
39
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
40
+ }
41
+ >(({ id, className, children, config, ...props }, ref) => {
42
+ const uniqueId = React.useId();
43
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
44
+
45
+ return (
46
+ <ChartContext.Provider value={{ config }}>
47
+ <div
48
+ data-chart={chartId}
49
+ ref={ref}
50
+ className={cn(
51
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
52
+ className,
53
+ )}
54
+ {...props}
55
+ >
56
+ <ChartStyle id={chartId} config={config} />
57
+ <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
58
+ </div>
59
+ </ChartContext.Provider>
60
+ );
61
+ });
62
+ ChartContainer.displayName = "Chart";
63
+
64
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
65
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
66
+
67
+ if (!colorConfig.length) {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <style
73
+ dangerouslySetInnerHTML={{
74
+ __html: Object.entries(THEMES)
75
+ .map(
76
+ ([theme, prefix]) => `
77
+ ${prefix} [data-chart=${id}] {
78
+ ${colorConfig
79
+ .map(([key, itemConfig]) => {
80
+ const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
81
+ return color ? ` --color-${key}: ${color};` : null;
82
+ })
83
+ .join("\n")}
84
+ }
85
+ `,
86
+ )
87
+ .join("\n"),
88
+ }}
89
+ />
90
+ );
91
+ };
92
+
93
+ const ChartTooltip = RechartsPrimitive.Tooltip;
94
+
95
+ const ChartTooltipContent = React.forwardRef<
96
+ HTMLDivElement,
97
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
98
+ React.ComponentProps<"div"> & {
99
+ hideLabel?: boolean;
100
+ hideIndicator?: boolean;
101
+ indicator?: "line" | "dot" | "dashed";
102
+ nameKey?: string;
103
+ labelKey?: string;
104
+ }
105
+ >(
106
+ (
107
+ {
108
+ active,
109
+ payload,
110
+ className,
111
+ indicator = "dot",
112
+ hideLabel = false,
113
+ hideIndicator = false,
114
+ label,
115
+ labelFormatter,
116
+ labelClassName,
117
+ formatter,
118
+ color,
119
+ nameKey,
120
+ labelKey,
121
+ },
122
+ ref,
123
+ ) => {
124
+ const { config } = useChart();
125
+
126
+ const tooltipLabel = React.useMemo(() => {
127
+ if (hideLabel || !payload?.length) {
128
+ return null;
129
+ }
130
+
131
+ const [item] = payload;
132
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
133
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
134
+ const value =
135
+ !labelKey && typeof label === "string"
136
+ ? config[label as keyof typeof config]?.label || label
137
+ : itemConfig?.label;
138
+
139
+ if (labelFormatter) {
140
+ return (
141
+ <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
142
+ );
143
+ }
144
+
145
+ if (!value) {
146
+ return null;
147
+ }
148
+
149
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
150
+ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
151
+
152
+ if (!active || !payload?.length) {
153
+ return null;
154
+ }
155
+
156
+ const nestLabel = payload.length === 1 && indicator !== "dot";
157
+
158
+ return (
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
163
+ className,
164
+ )}
165
+ >
166
+ {!nestLabel ? tooltipLabel : null}
167
+ <div className="grid gap-1.5">
168
+ {payload
169
+ .filter((item) => item.type !== "none")
170
+ .map((item, index) => {
171
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
172
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
173
+ const indicatorColor = color || item.payload.fill || item.color;
174
+
175
+ return (
176
+ <div
177
+ key={item.dataKey}
178
+ className={cn(
179
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
180
+ indicator === "dot" && "items-center",
181
+ )}
182
+ >
183
+ {formatter && item?.value !== undefined && item.name ? (
184
+ formatter(item.value, item.name, item, index, item.payload)
185
+ ) : (
186
+ <>
187
+ {itemConfig?.icon ? (
188
+ <itemConfig.icon />
189
+ ) : (
190
+ !hideIndicator && (
191
+ <div
192
+ className={cn(
193
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
194
+ {
195
+ "h-2.5 w-2.5": indicator === "dot",
196
+ "w-1": indicator === "line",
197
+ "w-0 border-[1.5px] border-dashed bg-transparent":
198
+ indicator === "dashed",
199
+ "my-0.5": nestLabel && indicator === "dashed",
200
+ },
201
+ )}
202
+ style={
203
+ {
204
+ "--color-bg": indicatorColor,
205
+ "--color-border": indicatorColor,
206
+ } as React.CSSProperties
207
+ }
208
+ />
209
+ )
210
+ )}
211
+ <div
212
+ className={cn(
213
+ "flex flex-1 justify-between leading-none",
214
+ nestLabel ? "items-end" : "items-center",
215
+ )}
216
+ >
217
+ <div className="grid gap-1.5">
218
+ {nestLabel ? tooltipLabel : null}
219
+ <span className="text-muted-foreground">
220
+ {itemConfig?.label || item.name}
221
+ </span>
222
+ </div>
223
+ {item.value && (
224
+ <span className="font-mono font-medium tabular-nums text-foreground">
225
+ {item.value.toLocaleString()}
226
+ </span>
227
+ )}
228
+ </div>
229
+ </>
230
+ )}
231
+ </div>
232
+ );
233
+ })}
234
+ </div>
235
+ </div>
236
+ );
237
+ },
238
+ );
239
+ ChartTooltipContent.displayName = "ChartTooltip";
240
+
241
+ const ChartLegend = RechartsPrimitive.Legend;
242
+
243
+ const ChartLegendContent = React.forwardRef<
244
+ HTMLDivElement,
245
+ React.ComponentProps<"div"> &
246
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
247
+ hideIcon?: boolean;
248
+ nameKey?: string;
249
+ }
250
+ >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
251
+ const { config } = useChart();
252
+
253
+ if (!payload?.length) {
254
+ return null;
255
+ }
256
+
257
+ return (
258
+ <div
259
+ ref={ref}
260
+ className={cn(
261
+ "flex items-center justify-center gap-4",
262
+ verticalAlign === "top" ? "pb-3" : "pt-3",
263
+ className,
264
+ )}
265
+ >
266
+ {payload
267
+ .filter((item) => item.type !== "none")
268
+ .map((item) => {
269
+ const key = `${nameKey || item.dataKey || "value"}`;
270
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
271
+
272
+ return (
273
+ <div
274
+ key={item.value}
275
+ className={cn(
276
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
277
+ )}
278
+ >
279
+ {itemConfig?.icon && !hideIcon ? (
280
+ <itemConfig.icon />
281
+ ) : (
282
+ <div
283
+ className="h-2 w-2 shrink-0 rounded-[2px]"
284
+ style={{
285
+ backgroundColor: item.color,
286
+ }}
287
+ />
288
+ )}
289
+ {itemConfig?.label}
290
+ </div>
291
+ );
292
+ })}
293
+ </div>
294
+ );
295
+ });
296
+ ChartLegendContent.displayName = "ChartLegend";
297
+
298
+ // Helper to extract item config from a payload.
299
+ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
300
+ if (typeof payload !== "object" || payload === null) {
301
+ return undefined;
302
+ }
303
+
304
+ const payloadPayload =
305
+ "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
306
+ ? payload.payload
307
+ : undefined;
308
+
309
+ let configLabelKey: string = key;
310
+
311
+ if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
312
+ configLabelKey = payload[key as keyof typeof payload] as string;
313
+ } else if (
314
+ payloadPayload &&
315
+ key in payloadPayload &&
316
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
317
+ ) {
318
+ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
319
+ }
320
+
321
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
322
+ }
323
+
324
+ export {
325
+ ChartContainer,
326
+ ChartTooltip,
327
+ ChartTooltipContent,
328
+ ChartLegend,
329
+ ChartLegendContent,
330
+ ChartStyle,
331
+ };
src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
20
+ <Check className="h-4 w-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ));
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
+
26
+ export { Checkbox };
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root;
6
+
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8
+
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10
+
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
src/components/ui/command.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { type DialogProps } from "@radix-ui/react-dialog";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { Search } from "lucide-react";
7
+
8
+ import { cn } from "@/lib/utils";
9
+ import { Dialog, DialogContent } from "@/components/ui/dialog";
10
+
11
+ const Command = React.forwardRef<
12
+ React.ElementRef<typeof CommandPrimitive>,
13
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
+ >(({ className, ...props }, ref) => (
15
+ <CommandPrimitive
16
+ ref={ref}
17
+ className={cn(
18
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ ));
24
+ Command.displayName = CommandPrimitive.displayName;
25
+
26
+ const CommandDialog = ({ children, ...props }: DialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="overflow-hidden p-0">
30
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ );
36
+ };
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ));
54
+
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
56
+
57
+ const CommandList = React.forwardRef<
58
+ React.ElementRef<typeof CommandPrimitive.List>,
59
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
+ >(({ className, ...props }, ref) => (
61
+ <CommandPrimitive.List
62
+ ref={ref}
63
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
+ {...props}
65
+ />
66
+ ));
67
+
68
+ CommandList.displayName = CommandPrimitive.List.displayName;
69
+
70
+ const CommandEmpty = React.forwardRef<
71
+ React.ElementRef<typeof CommandPrimitive.Empty>,
72
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
+ >((props, ref) => (
74
+ <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
75
+ ));
76
+
77
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
78
+
79
+ const CommandGroup = React.forwardRef<
80
+ React.ElementRef<typeof CommandPrimitive.Group>,
81
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
82
+ >(({ className, ...props }, ref) => (
83
+ <CommandPrimitive.Group
84
+ ref={ref}
85
+ className={cn(
86
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+
93
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
94
+
95
+ const CommandSeparator = React.forwardRef<
96
+ React.ElementRef<typeof CommandPrimitive.Separator>,
97
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
98
+ >(({ className, ...props }, ref) => (
99
+ <CommandPrimitive.Separator
100
+ ref={ref}
101
+ className={cn("-mx-1 h-px bg-border", className)}
102
+ {...props}
103
+ />
104
+ ));
105
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
106
+
107
+ const CommandItem = React.forwardRef<
108
+ React.ElementRef<typeof CommandPrimitive.Item>,
109
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
110
+ >(({ className, ...props }, ref) => (
111
+ <CommandPrimitive.Item
112
+ ref={ref}
113
+ className={cn(
114
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ ));
120
+
121
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
122
+
123
+ const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
124
+ return (
125
+ <span
126
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
127
+ {...props}
128
+ />
129
+ );
130
+ };
131
+ CommandShortcut.displayName = "CommandShortcut";
132
+
133
+ export {
134
+ Command,
135
+ CommandDialog,
136
+ CommandInput,
137
+ CommandList,
138
+ CommandEmpty,
139
+ CommandGroup,
140
+ CommandItem,
141
+ CommandShortcut,
142
+ CommandSeparator,
143
+ };
src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
3
+ import { Check, ChevronRight, Circle } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root;
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean;
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ));
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ ));
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ));
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean;
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ ));
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className,
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ));
111
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
112
+
113
+ const ContextMenuRadioItem = React.forwardRef<
114
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
115
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
116
+ >(({ className, children, ...props }, ref) => (
117
+ <ContextMenuPrimitive.RadioItem
118
+ ref={ref}
119
+ className={cn(
120
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
121
+ className,
122
+ )}
123
+ {...props}
124
+ >
125
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
126
+ <ContextMenuPrimitive.ItemIndicator>
127
+ <Circle className="h-4 w-4 fill-current" />
128
+ </ContextMenuPrimitive.ItemIndicator>
129
+ </span>
130
+ {children}
131
+ </ContextMenuPrimitive.RadioItem>
132
+ ));
133
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
134
+
135
+ const ContextMenuLabel = React.forwardRef<
136
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
137
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
138
+ inset?: boolean;
139
+ }
140
+ >(({ className, inset, ...props }, ref) => (
141
+ <ContextMenuPrimitive.Label
142
+ ref={ref}
143
+ className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
144
+ {...props}
145
+ />
146
+ ));
147
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
148
+
149
+ const ContextMenuSeparator = React.forwardRef<
150
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
151
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
152
+ >(({ className, ...props }, ref) => (
153
+ <ContextMenuPrimitive.Separator
154
+ ref={ref}
155
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
156
+ {...props}
157
+ />
158
+ ));
159
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
160
+
161
+ const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
162
+ return (
163
+ <span
164
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
165
+ {...props}
166
+ />
167
+ );
168
+ };
169
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
170
+
171
+ export {
172
+ ContextMenu,
173
+ ContextMenuTrigger,
174
+ ContextMenuContent,
175
+ ContextMenuItem,
176
+ ContextMenuCheckboxItem,
177
+ ContextMenuRadioItem,
178
+ ContextMenuLabel,
179
+ ContextMenuSeparator,
180
+ ContextMenuShortcut,
181
+ ContextMenuGroup,
182
+ ContextMenuPortal,
183
+ ContextMenuSub,
184
+ ContextMenuSubContent,
185
+ ContextMenuSubTrigger,
186
+ ContextMenuRadioGroup,
187
+ };
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const Dialog = DialogPrimitive.Root;
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger;
12
+
13
+ const DialogPortal = DialogPrimitive.Portal;
14
+
15
+ const DialogClose = DialogPrimitive.Close;
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ ));
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ));
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
55
+
56
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
57
+ <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
58
+ );
59
+ DialogHeader.displayName = "DialogHeader";
60
+
61
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
64
+ {...props}
65
+ />
66
+ );
67
+ DialogFooter.displayName = "DialogFooter";
68
+
69
+ const DialogTitle = React.forwardRef<
70
+ React.ElementRef<typeof DialogPrimitive.Title>,
71
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
72
+ >(({ className, ...props }, ref) => (
73
+ <DialogPrimitive.Title
74
+ ref={ref}
75
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
76
+ {...props}
77
+ />
78
+ ));
79
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
80
+
81
+ const DialogDescription = React.forwardRef<
82
+ React.ElementRef<typeof DialogPrimitive.Description>,
83
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
84
+ >(({ className, ...props }, ref) => (
85
+ <DialogPrimitive.Description
86
+ ref={ref}
87
+ className={cn("text-sm text-muted-foreground", className)}
88
+ {...props}
89
+ />
90
+ ));
91
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
92
+
93
+ export {
94
+ Dialog,
95
+ DialogPortal,
96
+ DialogOverlay,
97
+ DialogTrigger,
98
+ DialogClose,
99
+ DialogContent,
100
+ DialogHeader,
101
+ DialogFooter,
102
+ DialogTitle,
103
+ DialogDescription,
104
+ };
src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "vaul";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Drawer = ({
7
+ shouldScaleBackground = true,
8
+ ...props
9
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
10
+ <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
11
+ );
12
+ Drawer.displayName = "Drawer";
13
+
14
+ const DrawerTrigger = DrawerPrimitive.Trigger;
15
+
16
+ const DrawerPortal = DrawerPrimitive.Portal;
17
+
18
+ const DrawerClose = DrawerPrimitive.Close;
19
+
20
+ const DrawerOverlay = React.forwardRef<
21
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
22
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
23
+ >(({ className, ...props }, ref) => (
24
+ <DrawerPrimitive.Overlay
25
+ ref={ref}
26
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
27
+ {...props}
28
+ />
29
+ ));
30
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
31
+
32
+ const DrawerContent = React.forwardRef<
33
+ React.ElementRef<typeof DrawerPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DrawerPortal>
37
+ <DrawerOverlay />
38
+ <DrawerPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
47
+ {children}
48
+ </DrawerPrimitive.Content>
49
+ </DrawerPortal>
50
+ ));
51
+ DrawerContent.displayName = "DrawerContent";
52
+
53
+ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
54
+ <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
55
+ );
56
+ DrawerHeader.displayName = "DrawerHeader";
57
+
58
+ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
59
+ <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
60
+ );
61
+ DrawerFooter.displayName = "DrawerFooter";
62
+
63
+ const DrawerTitle = React.forwardRef<
64
+ React.ElementRef<typeof DrawerPrimitive.Title>,
65
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
66
+ >(({ className, ...props }, ref) => (
67
+ <DrawerPrimitive.Title
68
+ ref={ref}
69
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
70
+ {...props}
71
+ />
72
+ ));
73
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
74
+
75
+ const DrawerDescription = React.forwardRef<
76
+ React.ElementRef<typeof DrawerPrimitive.Description>,
77
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
78
+ >(({ className, ...props }, ref) => (
79
+ <DrawerPrimitive.Description
80
+ ref={ref}
81
+ className={cn("text-sm text-muted-foreground", className)}
82
+ {...props}
83
+ />
84
+ ));
85
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
86
+
87
+ export {
88
+ Drawer,
89
+ DrawerPortal,
90
+ DrawerOverlay,
91
+ DrawerTrigger,
92
+ DrawerClose,
93
+ DrawerContent,
94
+ DrawerHeader,
95
+ DrawerFooter,
96
+ DrawerTitle,
97
+ DrawerDescription,
98
+ };
src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const DropdownMenu = DropdownMenuPrimitive.Root;
10
+
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12
+
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
+
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16
+
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
+
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20
+
21
+ const DropdownMenuSubTrigger = React.forwardRef<
22
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean;
25
+ }
26
+ >(({ className, inset, children, ...props }, ref) => (
27
+ <DropdownMenuPrimitive.SubTrigger
28
+ ref={ref}
29
+ className={cn(
30
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
+ inset && "pl-8",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronRight className="ml-auto" />
38
+ </DropdownMenuPrimitive.SubTrigger>
39
+ ));
40
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
41
+
42
+ const DropdownMenuSubContent = React.forwardRef<
43
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
44
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
45
+ >(({ className, ...props }, ref) => (
46
+ <DropdownMenuPrimitive.SubContent
47
+ ref={ref}
48
+ className={cn(
49
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ));
55
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
67
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ </DropdownMenuPrimitive.Portal>
73
+ ));
74
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
75
+
76
+ const DropdownMenuItem = React.forwardRef<
77
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
78
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
79
+ inset?: boolean;
80
+ }
81
+ >(({ className, inset, ...props }, ref) => (
82
+ <DropdownMenuPrimitive.Item
83
+ ref={ref}
84
+ className={cn(
85
+ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
86
+ inset && "pl-8",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ ));
92
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
93
+
94
+ const DropdownMenuCheckboxItem = React.forwardRef<
95
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
96
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
97
+ >(({ className, children, checked, ...props }, ref) => (
98
+ <DropdownMenuPrimitive.CheckboxItem
99
+ ref={ref}
100
+ className={cn(
101
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
102
+ className,
103
+ )}
104
+ checked={checked}
105
+ {...props}
106
+ >
107
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
108
+ <DropdownMenuPrimitive.ItemIndicator>
109
+ <Check className="h-4 w-4" />
110
+ </DropdownMenuPrimitive.ItemIndicator>
111
+ </span>
112
+ {children}
113
+ </DropdownMenuPrimitive.CheckboxItem>
114
+ ));
115
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125
+ className,
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ));
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean;
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
148
+ {...props}
149
+ />
150
+ ));
151
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
152
+
153
+ const DropdownMenuSeparator = React.forwardRef<
154
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
155
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
156
+ >(({ className, ...props }, ref) => (
157
+ <DropdownMenuPrimitive.Separator
158
+ ref={ref}
159
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
160
+ {...props}
161
+ />
162
+ ));
163
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
164
+
165
+ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
166
+ return (
167
+ <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
168
+ );
169
+ };
170
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
171
+
172
+ export {
173
+ DropdownMenu,
174
+ DropdownMenuTrigger,
175
+ DropdownMenuContent,
176
+ DropdownMenuItem,
177
+ DropdownMenuCheckboxItem,
178
+ DropdownMenuRadioItem,
179
+ DropdownMenuLabel,
180
+ DropdownMenuSeparator,
181
+ DropdownMenuShortcut,
182
+ DropdownMenuGroup,
183
+ DropdownMenuPortal,
184
+ DropdownMenuSub,
185
+ DropdownMenuSubContent,
186
+ DropdownMenuSubTrigger,
187
+ DropdownMenuRadioGroup,
188
+ };
src/components/ui/form.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import {
5
+ Controller,
6
+ FormProvider,
7
+ useFormContext,
8
+ type ControllerProps,
9
+ type FieldPath,
10
+ type FieldValues,
11
+ } from "react-hook-form";
12
+
13
+ import { cn } from "@/lib/utils";
14
+ import { Label } from "@/components/ui/label";
15
+
16
+ const Form = FormProvider;
17
+
18
+ type FormFieldContextValue<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ > = {
22
+ name: TName;
23
+ };
24
+
25
+ const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
26
+
27
+ const FormField = <
28
+ TFieldValues extends FieldValues = FieldValues,
29
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
30
+ >({
31
+ ...props
32
+ }: ControllerProps<TFieldValues, TName>) => {
33
+ return (
34
+ <FormFieldContext.Provider value={{ name: props.name }}>
35
+ <Controller {...props} />
36
+ </FormFieldContext.Provider>
37
+ );
38
+ };
39
+
40
+ const useFormField = () => {
41
+ const fieldContext = React.useContext(FormFieldContext);
42
+ const itemContext = React.useContext(FormItemContext);
43
+ const { getFieldState, formState } = useFormContext();
44
+
45
+ if (!fieldContext) {
46
+ throw new Error("useFormField should be used within <FormField>");
47
+ }
48
+
49
+ if (!itemContext) {
50
+ throw new Error("useFormField should be used within <FormItem>");
51
+ }
52
+
53
+ const fieldState = getFieldState(fieldContext.name, formState);
54
+
55
+ const { id } = itemContext;
56
+
57
+ return {
58
+ id,
59
+ name: fieldContext.name,
60
+ formItemId: `${id}-form-item`,
61
+ formDescriptionId: `${id}-form-item-description`,
62
+ formMessageId: `${id}-form-item-message`,
63
+ ...fieldState,
64
+ };
65
+ };
66
+
67
+ type FormItemContextValue = {
68
+ id: string;
69
+ };
70
+
71
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null);
72
+
73
+ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
74
+ ({ className, ...props }, ref) => {
75
+ const id = React.useId();
76
+
77
+ return (
78
+ <FormItemContext.Provider value={{ id }}>
79
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
80
+ </FormItemContext.Provider>
81
+ );
82
+ },
83
+ );
84
+ FormItem.displayName = "FormItem";
85
+
86
+ const FormLabel = React.forwardRef<
87
+ React.ElementRef<typeof LabelPrimitive.Root>,
88
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
89
+ >(({ className, ...props }, ref) => {
90
+ const { error, formItemId } = useFormField();
91
+
92
+ return (
93
+ <Label
94
+ ref={ref}
95
+ className={cn(error && "text-destructive", className)}
96
+ htmlFor={formItemId}
97
+ {...props}
98
+ />
99
+ );
100
+ });
101
+ FormLabel.displayName = "FormLabel";
102
+
103
+ const FormControl = React.forwardRef<
104
+ React.ElementRef<typeof Slot>,
105
+ React.ComponentPropsWithoutRef<typeof Slot>
106
+ >(({ ...props }, ref) => {
107
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
108
+
109
+ return (
110
+ <Slot
111
+ ref={ref}
112
+ id={formItemId}
113
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
114
+ aria-invalid={!!error}
115
+ {...props}
116
+ />
117
+ );
118
+ });
119
+ FormControl.displayName = "FormControl";
120
+
121
+ const FormDescription = React.forwardRef<
122
+ HTMLParagraphElement,
123
+ React.HTMLAttributes<HTMLParagraphElement>
124
+ >(({ className, ...props }, ref) => {
125
+ const { formDescriptionId } = useFormField();
126
+
127
+ return (
128
+ <p
129
+ ref={ref}
130
+ id={formDescriptionId}
131
+ className={cn("text-[0.8rem] text-muted-foreground", className)}
132
+ {...props}
133
+ />
134
+ );
135
+ });
136
+ FormDescription.displayName = "FormDescription";
137
+
138
+ const FormMessage = React.forwardRef<
139
+ HTMLParagraphElement,
140
+ React.HTMLAttributes<HTMLParagraphElement>
141
+ >(({ className, children, ...props }, ref) => {
142
+ const { error, formMessageId } = useFormField();
143
+ const body = error ? String(error?.message ?? "") : children;
144
+
145
+ if (!body) {
146
+ return null;
147
+ }
148
+
149
+ return (
150
+ <p
151
+ ref={ref}
152
+ id={formMessageId}
153
+ className={cn("text-[0.8rem] font-medium text-destructive", className)}
154
+ {...props}
155
+ >
156
+ {body}
157
+ </p>
158
+ );
159
+ });
160
+ FormMessage.displayName = "FormMessage";
161
+
162
+ export {
163
+ useFormField,
164
+ Form,
165
+ FormItem,
166
+ FormLabel,
167
+ FormControl,
168
+ FormDescription,
169
+ FormMessage,
170
+ FormField,
171
+ };
src/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const HoverCard = HoverCardPrimitive.Root;
7
+
8
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
9
+
10
+ const HoverCardContent = React.forwardRef<
11
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
12
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
13
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14
+ <HoverCardPrimitive.Content
15
+ ref={ref}
16
+ align={align}
17
+ sideOffset={sideOffset}
18
+ className={cn(
19
+ "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ ));
25
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
26
+
27
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
src/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { OTPInput, OTPInputContext } from "input-otp";
3
+ import { Minus } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const InputOTP = React.forwardRef<
8
+ React.ElementRef<typeof OTPInput>,
9
+ React.ComponentPropsWithoutRef<typeof OTPInput>
10
+ >(({ className, containerClassName, ...props }, ref) => (
11
+ <OTPInput
12
+ ref={ref}
13
+ containerClassName={cn(
14
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
15
+ containerClassName,
16
+ )}
17
+ className={cn("disabled:cursor-not-allowed", className)}
18
+ {...props}
19
+ />
20
+ ));
21
+ InputOTP.displayName = "InputOTP";
22
+
23
+ const InputOTPGroup = React.forwardRef<
24
+ React.ElementRef<"div">,
25
+ React.ComponentPropsWithoutRef<"div">
26
+ >(({ className, ...props }, ref) => (
27
+ <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
+ ));
29
+ InputOTPGroup.displayName = "InputOTPGroup";
30
+
31
+ const InputOTPSlot = React.forwardRef<
32
+ React.ElementRef<"div">,
33
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
34
+ >(({ index, className, ...props }, ref) => {
35
+ const inputOTPContext = React.useContext(OTPInputContext);
36
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ className={cn(
42
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
43
+ isActive && "z-10 ring-1 ring-ring",
44
+ className,
45
+ )}
46
+ {...props}
47
+ >
48
+ {char}
49
+ {hasFakeCaret && (
50
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51
+ <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52
+ </div>
53
+ )}
54
+ </div>
55
+ );
56
+ });
57
+ InputOTPSlot.displayName = "InputOTPSlot";
58
+
59
+ const InputOTPSeparator = React.forwardRef<
60
+ React.ElementRef<"div">,
61
+ React.ComponentPropsWithoutRef<"div">
62
+ >(({ ...props }, ref) => (
63
+ <div ref={ref} role="separator" {...props}>
64
+ <Minus />
65
+ </div>
66
+ ));
67
+ InputOTPSeparator.displayName = "InputOTPSeparator";
68
+
69
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
src/components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className,
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
20
+ Input.displayName = "Input";
21
+
22
+ export { Input };
src/components/ui/label.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const labelVariants = cva(
10
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11
+ );
12
+
13
+ const Label = React.forwardRef<
14
+ React.ElementRef<typeof LabelPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
16
+ >(({ className, ...props }, ref) => (
17
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
18
+ ));
19
+ Label.displayName = LabelPrimitive.Root.displayName;
20
+
21
+ export { Label };