underrate commited on
Commit
34ed5b1
·
verified ·
1 Parent(s): 14e4ee5

Initial commit

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 +45 -0
  2. app/(public)/about/page.tsx +84 -0
  3. app/(public)/blog/[slug]/page.tsx +280 -0
  4. app/(public)/blog/page.tsx +99 -0
  5. app/(public)/contact/page.tsx +85 -0
  6. app/(public)/layout.tsx +16 -0
  7. app/(public)/page.tsx +19 -0
  8. app/(public)/portfolio/[slug]/page.tsx +156 -0
  9. app/(public)/portfolio/page.tsx +50 -0
  10. app/(public)/quote/page.tsx +524 -0
  11. app/(public)/services/page.tsx +33 -0
  12. app/admin/blog/[id]/page.tsx +66 -0
  13. app/admin/blog/new/page.tsx +17 -0
  14. app/admin/blog/page.tsx +179 -0
  15. app/admin/dashboard/page.tsx +78 -0
  16. app/admin/forgot-password/page.tsx +80 -0
  17. app/admin/inquiries/[id]/page.tsx +266 -0
  18. app/admin/inquiries/page.tsx +150 -0
  19. app/admin/login/page.tsx +142 -0
  20. app/admin/page.tsx +5 -0
  21. app/admin/portfolio/page.tsx +478 -0
  22. app/admin/posts/page.tsx +18 -0
  23. app/admin/reset-password/page.tsx +130 -0
  24. app/admin/settings/page.tsx +20 -0
  25. app/admin/testimonials/page.tsx +395 -0
  26. app/api/auth/[...nextauth]/route.ts +2 -0
  27. app/favicon.ico +0 -0
  28. app/globals.css +1494 -0
  29. app/layout.tsx +81 -0
  30. app/unauthorized/page.tsx +31 -0
  31. components.json +23 -0
  32. components/admin/BlogForm.tsx +281 -0
  33. components/auth/RoleGuard.tsx +46 -0
  34. components/home/CTASection.tsx +231 -0
  35. components/home/FeaturedPortfolio.tsx +252 -0
  36. components/home/HeroSection.tsx +263 -0
  37. components/home/ServicesOverview.tsx +256 -0
  38. components/home/TestimonialsCarousel.tsx +328 -0
  39. components/home/TrustIndicators.tsx +128 -0
  40. components/layout/Footer.tsx +205 -0
  41. components/layout/Header.tsx +185 -0
  42. components/mode-toggle.tsx +40 -0
  43. components/portfolio/ProjectGallery.tsx +106 -0
  44. components/services/AddOnServices.tsx +69 -0
  45. components/services/FAQSection.tsx +111 -0
  46. components/services/PackageComparison.tsx +124 -0
  47. components/services/ProcessTimeline.tsx +82 -0
  48. components/services/ServiceDescriptions.tsx +87 -0
  49. components/theme-provider.tsx +11 -0
  50. components/ui/accordion.tsx +66 -0
.gitignore ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ .env.local
36
+ .env.development.local
37
+ .env.test.local
38
+ .env.production.local
39
+
40
+ # vercel
41
+ .vercel
42
+
43
+ # typescript
44
+ *.tsbuildinfo
45
+ next-env.d.ts
app/(public)/about/page.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Users, Target, Award, Zap } from "lucide-react"
2
+
3
+ const values = [
4
+ {
5
+ icon: Target,
6
+ title: "Mission-Driven",
7
+ description: "Every project starts with understanding your goals. We align our expertise with your vision to deliver measurable results.",
8
+ },
9
+ {
10
+ icon: Users,
11
+ title: "Collaborative",
12
+ description: "We believe the best products are built together. You're involved at every step — from strategy to launch.",
13
+ },
14
+ {
15
+ icon: Award,
16
+ title: "Quality Obsessed",
17
+ description: "We don't cut corners. Every pixel, every line of code, every interaction is crafted to the highest standard.",
18
+ },
19
+ {
20
+ icon: Zap,
21
+ title: "Future-Ready",
22
+ description: "We build with tomorrow in mind — scalable architectures, modern frameworks, and forward-thinking design.",
23
+ },
24
+ ]
25
+
26
+ export default function AboutPage() {
27
+ return (
28
+ <div className="py-20">
29
+ <div className="container">
30
+ {/* Hero */}
31
+ <div className="animate-fade-in-up text-center mb-20 max-w-3xl mx-auto">
32
+ <p className="text-sm font-semibold text-primary uppercase tracking-[0.15em] mb-3">About Us</p>
33
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl mb-6">
34
+ A small team with{" "}
35
+ <span className="text-primary">big ambitions</span>
36
+ </h1>
37
+ <p className="text-lg text-muted-foreground leading-relaxed">
38
+ We&apos;re a passionate team of designers, developers, and strategists who love turning ideas into
39
+ exceptional digital products. Founded with a simple belief: great software should be accessible to everyone.
40
+ </p>
41
+ </div>
42
+
43
+ {/* Stats */}
44
+ <div className="animate-fade-in-up animate-delay-2 grid grid-cols-2 md:grid-cols-4 gap-8 mb-20">
45
+ {[
46
+ { value: "5+", label: "Years" },
47
+ { value: "50+", label: "Projects" },
48
+ { value: "30+", label: "Happy Clients" },
49
+ { value: "98%", label: "Satisfaction" },
50
+ ].map((stat) => (
51
+ <div key={stat.label} className="text-center p-6 rounded-xl border bg-card">
52
+ <div className="text-3xl font-bold text-primary mb-1">
53
+ {stat.value}
54
+ </div>
55
+ <div className="text-sm text-muted-foreground">{stat.label}</div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+
60
+ {/* Values */}
61
+ <div className="animate-fade-in-up animate-delay-3">
62
+ <h2 className="text-2xl font-bold text-center mb-12">What We Stand For</h2>
63
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
64
+ {values.map((value, i) => (
65
+ <div
66
+ key={value.title}
67
+ className="animate-fade-in-up flex gap-4 p-6 rounded-xl border bg-card hover:border-primary/20 hover:shadow-lg transition-all duration-500"
68
+ style={{ animationDelay: `${0.4 + i * 0.1}s` }}
69
+ >
70
+ <div className="shrink-0 w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
71
+ <value.icon className="w-6 h-6" suppressHydrationWarning />
72
+ </div>
73
+ <div>
74
+ <h3 className="font-semibold mb-1">{value.title}</h3>
75
+ <p className="text-sm text-muted-foreground leading-relaxed">{value.description}</p>
76
+ </div>
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ )
84
+ }
app/(public)/blog/[slug]/page.tsx ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Metadata } from 'next'
2
+ import { notFound } from 'next/navigation'
3
+ import Link from 'next/link'
4
+ import { ArrowLeft, ArrowRight, CalendarDays, Share2, Linkedin, Twitter, Link as LinkIcon } from 'lucide-react'
5
+ import { format } from 'date-fns'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8
+
9
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api'
10
+
11
+ // Define the shape of our BlogPost
12
+ interface BlogPost {
13
+ id: string
14
+ title: string
15
+ slug: string
16
+ content: string
17
+ excerpt: string | null
18
+ featuredImage: string | null
19
+ tags: string | null
20
+ publishedAt: string | null
21
+ createdAt: string
22
+ author: {
23
+ name: string | null
24
+ }
25
+ }
26
+
27
+ // Fetch a single post by slug
28
+ async function getPost(slug: string): Promise<BlogPost | null> {
29
+ try {
30
+ const res = await fetch(`${API_URL}/blog/${slug}`, {
31
+ next: { revalidate: 60 } // Revalidate every minute
32
+ })
33
+ if (!res.ok) {
34
+ if (res.status === 404) return null
35
+ throw new Error('Failed to fetch post')
36
+ }
37
+ return res.json()
38
+ } catch (error) {
39
+ console.error("Error fetching post:", error)
40
+ return null
41
+ }
42
+ }
43
+
44
+ // Fetch related posts
45
+ async function getRelated(slug: string): Promise<BlogPost[]> {
46
+ try {
47
+ const res = await fetch(`${API_URL}/blog/${slug}/related`, {
48
+ next: { revalidate: 60 }
49
+ })
50
+ if (!res.ok) return []
51
+ return res.json()
52
+ } catch {
53
+ return []
54
+ }
55
+ }
56
+
57
+ // Dynamically generate metadata for SEO
58
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
59
+ const resolvedParams = await params;
60
+ const post = await getPost(resolvedParams.slug)
61
+
62
+ if (!post) {
63
+ return {
64
+ title: 'Post Not Found | NexaFlow',
65
+ }
66
+ }
67
+
68
+ const plainTextExcerpt = post.excerpt || post.content.replace(/<[^>]*>?/gm, '').substring(0, 160)
69
+
70
+ return {
71
+ title: `${post.title} | NexaFlow Blog`,
72
+ description: plainTextExcerpt,
73
+ openGraph: {
74
+ title: post.title,
75
+ description: plainTextExcerpt,
76
+ type: 'article',
77
+ publishedTime: post.publishedAt || post.createdAt,
78
+ authors: [post.author?.name || 'NexaFlow Team'],
79
+ images: post.featuredImage ? [{ url: post.featuredImage }] : [],
80
+ },
81
+ twitter: {
82
+ card: 'summary_large_image',
83
+ title: post.title,
84
+ description: plainTextExcerpt,
85
+ images: post.featuredImage ? [post.featuredImage] : [],
86
+ }
87
+ }
88
+ }
89
+
90
+ export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
91
+ const resolvedParams = await params;
92
+ const post = await getPost(resolvedParams.slug)
93
+
94
+ if (!post) {
95
+ notFound()
96
+ }
97
+
98
+ const relatedPosts = await getRelated(resolvedParams.slug)
99
+
100
+ const publishDate = post.publishedAt ? new Date(post.publishedAt) : new Date(post.createdAt)
101
+ const jsonLd = {
102
+ '@context': 'https://schema.org',
103
+ '@type': 'BlogPosting',
104
+ headline: post.title,
105
+ image: post.featuredImage ? [post.featuredImage] : [],
106
+ datePublished: publishDate.toISOString(),
107
+ author: {
108
+ '@type': 'Person',
109
+ name: post.author?.name || 'NexaFlow Team',
110
+ },
111
+ }
112
+
113
+ const tags = post.tags ? JSON.parse(post.tags) : []
114
+
115
+ return (
116
+ <article className="py-20 animate-fade-in-up">
117
+ {/* Inject JSON-LD for SEO */}
118
+ <script
119
+ type="application/ld+json"
120
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
121
+ />
122
+
123
+ <div className="container max-w-4xl">
124
+ {/* Back Link */}
125
+ <div className="mb-10">
126
+ <Button variant="ghost" asChild className="pl-0 hover:bg-transparent hover:text-primary">
127
+ <Link href="/blog">
128
+ <ArrowLeft className="mr-2 h-4 w-4" suppressHydrationWarning />
129
+ Back to Blog
130
+ </Link>
131
+ </Button>
132
+ </div>
133
+
134
+ {/* Header */}
135
+ <header className="mb-12 text-center md:text-left">
136
+ {tags.length > 0 && (
137
+ <div className="flex flex-wrap items-center justify-center md:justify-start gap-2 mb-4">
138
+ {tags.map((tag: string) => (
139
+ <span key={tag} className="text-xs font-semibold px-2.5 py-1 rounded-full bg-primary/10 text-primary">
140
+ {tag}
141
+ </span>
142
+ ))}
143
+ </div>
144
+ )}
145
+
146
+ <h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-6">
147
+ {post.title}
148
+ </h1>
149
+
150
+ <div className="flex flex-col sm:flex-row items-center justify-center md:justify-start gap-4 text-sm text-muted-foreground">
151
+ <div className="flex items-center gap-2">
152
+ <span className="font-medium text-foreground">{post.author?.name || 'NexaFlow Team'}</span>
153
+ </div>
154
+ <span className="hidden sm:inline">•</span>
155
+ <div className="flex items-center gap-1.5">
156
+ <CalendarDays className="h-4 w-4" suppressHydrationWarning />
157
+ <time dateTime={publishDate.toISOString()}>
158
+ {format(publishDate, 'MMMM d, yyyy')}
159
+ </time>
160
+ </div>
161
+ </div>
162
+ </header>
163
+
164
+ {/* Featured Image */}
165
+ {post.featuredImage && (
166
+ <div className="relative w-full aspect-[16/9] mb-12 rounded-2xl overflow-hidden bg-muted">
167
+ {/* eslint-disable-next-line @next/next/no-img-element */}
168
+ <img
169
+ src={post.featuredImage}
170
+ alt={post.title}
171
+ className="object-cover w-full h-full"
172
+ />
173
+ </div>
174
+ )}
175
+
176
+ {/* Content & Sidebar Layout */}
177
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
178
+
179
+ {/* Share Sidebar (Desktop) */}
180
+ <div className="hidden lg:block lg:col-span-1">
181
+ <div className="sticky top-24 flex flex-col gap-4">
182
+ <div className="h-px w-full bg-border mb-2" />
183
+ <p className="text-xs font-medium uppercase text-muted-foreground mb-1">Share</p>
184
+ <Button variant="outline" size="icon" className="rounded-full w-10 h-10 shrink-0" aria-label="Share on Twitter">
185
+ <Twitter className="h-4 w-4" suppressHydrationWarning />
186
+ </Button>
187
+ <Button variant="outline" size="icon" className="rounded-full w-10 h-10 shrink-0" aria-label="Share on LinkedIn">
188
+ <Linkedin className="h-4 w-4" suppressHydrationWarning />
189
+ </Button>
190
+ <Button variant="outline" size="icon" className="rounded-full w-10 h-10 shrink-0" aria-label="Copy link to clipboard">
191
+ <LinkIcon className="h-4 w-4" suppressHydrationWarning />
192
+ </Button>
193
+ </div>
194
+ </div>
195
+
196
+ {/* Main Content */}
197
+ <div className="lg:col-span-9 prose prose-lg dark:prose-invert max-w-none prose-headings:font-bold prose-a:text-primary hover:prose-a:text-primary/80 prose-img:rounded-xl">
198
+ {/* We use dangerouslySetInnerHTML to render the rich text (HTML generated by the frontend editor later) */}
199
+ <div dangerouslySetInnerHTML={{ __html: post.content }} />
200
+ </div>
201
+ </div>
202
+
203
+ {/* Mobile Share (Bottom) */}
204
+ <div className="mt-12 pt-8 border-t lg:hidden">
205
+ <p className="text-sm font-medium mb-4">Share this article</p>
206
+ <div className="flex gap-3">
207
+ <Button variant="outline" size="sm" aria-label="Share on Twitter">
208
+ <Twitter className="mr-2 h-4 w-4" suppressHydrationWarning /> Twitter
209
+ </Button>
210
+ <Button variant="outline" size="sm" aria-label="Share on LinkedIn">
211
+ <Linkedin className="mr-2 h-4 w-4" suppressHydrationWarning /> LinkedIn
212
+ </Button>
213
+ <Button variant="outline" size="sm" aria-label="Copy link to clipboard">
214
+ <Share2 className="mr-2 h-4 w-4" suppressHydrationWarning /> Copy Link
215
+ </Button>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Related Articles Matrix */}
220
+ {relatedPosts.length > 0 && (
221
+ <div className="mt-20 pt-16 border-t">
222
+ <div className="flex items-center justify-between mb-8">
223
+ <h2 className="text-3xl font-bold tracking-tight">Related Articles</h2>
224
+ <Button variant="ghost" asChild className="hidden sm:flex">
225
+ <Link href="/blog">
226
+ View all <ArrowRight className="ml-2 h-4 w-4" suppressHydrationWarning />
227
+ </Link>
228
+ </Button>
229
+ </div>
230
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
231
+ {relatedPosts.map((relatedPost) => {
232
+ const wordCount = relatedPost.content ? relatedPost.content.split(/\s+/).length : 0
233
+ const readTime = Math.max(1, Math.ceil(wordCount / 200))
234
+
235
+ const relatedTags = relatedPost.tags ? JSON.parse(relatedPost.tags) : []
236
+ const primaryCategory = relatedTags.length > 0 ? relatedTags[0] : "Article"
237
+
238
+ return (
239
+ <Link key={relatedPost.id} href={`/blog/${relatedPost.slug}`} className="w-full">
240
+ <Card className="h-full flex flex-col group border hover:border-primary/20 hover:shadow-lg transition-all duration-500 cursor-pointer overflow-hidden bg-background">
241
+ {relatedPost.featuredImage && (
242
+ <div className="h-40 w-full bg-muted overflow-hidden">
243
+ {/* eslint-disable-next-line @next/next/no-img-element */}
244
+ <img
245
+ src={relatedPost.featuredImage}
246
+ alt={relatedPost.title}
247
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
248
+ />
249
+ </div>
250
+ )}
251
+ <CardHeader className="flex-none pb-2">
252
+ <div className="flex items-center justify-between mb-2">
253
+ <span className="text-xs font-semibold text-primary uppercase tracking-wider">{primaryCategory}</span>
254
+ <span className="text-xs text-muted-foreground">{readTime} min read</span>
255
+ </div>
256
+ <CardTitle className="text-lg group-hover:text-primary transition-colors line-clamp-2">{relatedPost.title}</CardTitle>
257
+ </CardHeader>
258
+ <CardContent className="flex-grow flex flex-col justify-end pt-2">
259
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-auto">
260
+ <CalendarDays className="h-3.5 w-3.5" suppressHydrationWarning />
261
+ {relatedPost.publishedAt ? format(new Date(relatedPost.publishedAt), 'MMM dd, yyyy') : format(new Date(relatedPost.createdAt), 'MMM dd, yyyy')}
262
+ </div>
263
+ </CardContent>
264
+ </Card>
265
+ </Link>
266
+ )
267
+ })}
268
+ </div>
269
+ <Button variant="outline" asChild className="w-full mt-6 sm:hidden">
270
+ <Link href="/blog">
271
+ View all references
272
+ </Link>
273
+ </Button>
274
+ </div>
275
+ )}
276
+
277
+ </div>
278
+ </article>
279
+ )
280
+ }
app/(public)/blog/page.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
2
+ import { CalendarDays } from "lucide-react"
3
+ import Link from "next/link"
4
+ import { format } from "date-fns"
5
+ import { Metadata } from "next"
6
+
7
+ export const metadata: Metadata = {
8
+ title: "Blog | NexaFlow - Insights on Web Development & Design",
9
+ description: "Read our latest thoughts on Next.js, web development, UI/UX design, and building great digital products.",
10
+ }
11
+
12
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
13
+
14
+ async function getPosts() {
15
+ try {
16
+ const res = await fetch(`${API_URL}/blog`, {
17
+ next: { revalidate: 60 } // Revalidate every minute
18
+ })
19
+ if (!res.ok) return []
20
+ return res.json()
21
+ } catch (error) {
22
+ console.error("Failed to fetch blog posts:", error)
23
+ return []
24
+ }
25
+ }
26
+
27
+ export default async function BlogPage() {
28
+ const posts = await getPosts()
29
+
30
+ return (
31
+ <div className="py-20">
32
+ <div className="container">
33
+ <div className="animate-fade-in-up text-center mb-16 max-w-3xl mx-auto">
34
+ <p className="text-sm font-semibold text-primary uppercase tracking-[0.15em] mb-3">Blog</p>
35
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl mb-6">
36
+ Insights &{" "}
37
+ <span className="text-primary">ideas</span>
38
+ </h1>
39
+ <p className="text-lg text-muted-foreground leading-relaxed">
40
+ Thoughts on design, development, and building great digital products.
41
+ </p>
42
+ </div>
43
+
44
+ {posts.length === 0 ? (
45
+ <div className="text-center text-muted-foreground py-10">
46
+ <p>No published articles found.</p>
47
+ </div>
48
+ ) : (
49
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-5xl mx-auto">
50
+ {posts.map((post: any, i: number) => {
51
+ // Quick read time estimate: 1 min per 200 words
52
+ const wordCount = post.content ? post.content.split(/\s+/).length : 0
53
+ const readTime = Math.max(1, Math.ceil(wordCount / 200))
54
+
55
+ // Safe tags
56
+ const tags = post.tags ? JSON.parse(post.tags) : []
57
+ const primaryCategory = tags.length > 0 ? tags[0] : "Article"
58
+
59
+ return (
60
+ <div key={post.id} className="animate-fade-in-up flex" style={{ animationDelay: `${i * 0.1}s` }}>
61
+ <Link href={`/blog/${post.slug}`} className="w-full">
62
+ <Card className="h-full flex flex-col group border hover:border-primary/20 hover:shadow-lg transition-all duration-500 cursor-pointer overflow-hidden">
63
+ {post.featuredImage && (
64
+ <div className="h-48 w-full bg-muted overflow-hidden">
65
+ {/* eslint-disable-next-line @next/next/no-img-element */}
66
+ <img
67
+ src={post.featuredImage}
68
+ alt={post.title}
69
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
70
+ />
71
+ </div>
72
+ )}
73
+ <CardHeader className="flex-none">
74
+ <div className="flex items-center justify-between mb-2">
75
+ <span className="text-xs font-semibold text-primary uppercase tracking-wider">{primaryCategory}</span>
76
+ <span className="text-xs text-muted-foreground">{readTime} min read</span>
77
+ </div>
78
+ <CardTitle className="text-lg group-hover:text-primary transition-colors line-clamp-2">{post.title}</CardTitle>
79
+ </CardHeader>
80
+ <CardContent className="flex-grow flex flex-col justify-between">
81
+ <p className="text-sm text-muted-foreground leading-relaxed mb-4 line-clamp-3">
82
+ {post.excerpt || post.content.replace(/<[^>]*>?/gm, '').substring(0, 150) + "..."}
83
+ </p>
84
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-auto pt-4 border-t border-border/50">
85
+ <CalendarDays className="h-3.5 w-3.5" suppressHydrationWarning />
86
+ {post.publishedAt ? format(new Date(post.publishedAt), 'MMM dd, yyyy') : format(new Date(post.createdAt), 'MMM dd, yyyy')}
87
+ </div>
88
+ </CardContent>
89
+ </Card>
90
+ </Link>
91
+ </div>
92
+ )
93
+ })}
94
+ </div>
95
+ )}
96
+ </div>
97
+ </div>
98
+ )
99
+ }
app/(public)/contact/page.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Button } from "@/components/ui/button"
4
+ import { Mail, MapPin, Phone, Send } from "lucide-react"
5
+
6
+ export default function ContactPage() {
7
+ return (
8
+ <div className="py-20">
9
+ <div className="container">
10
+ <div className="animate-fade-in-up text-center mb-16 max-w-3xl mx-auto">
11
+ <p className="text-sm font-semibold text-primary uppercase tracking-[0.15em] mb-3">Contact</p>
12
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl mb-6">
13
+ Let&apos;s{" "}
14
+ <span className="text-primary">talk</span>
15
+ </h1>
16
+ <p className="text-lg text-muted-foreground leading-relaxed">
17
+ Have a project in mind? We&apos;d love to hear about it. Drop us a message and we&apos;ll get back to you within 24 hours.
18
+ </p>
19
+ </div>
20
+
21
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-12 max-w-5xl mx-auto">
22
+ {/* Contact info */}
23
+ <div className="animate-fade-in-up animate-delay-2 lg:col-span-2 flex flex-col gap-6">
24
+ <div className="flex items-start gap-4 p-4 rounded-xl border bg-card">
25
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0">
26
+ <Mail className="w-5 h-5" suppressHydrationWarning />
27
+ </div>
28
+ <div>
29
+ <h3 className="font-semibold text-sm">Email</h3>
30
+ <p className="text-sm text-muted-foreground">hello@nexaflow.dev</p>
31
+ </div>
32
+ </div>
33
+ <div className="flex items-start gap-4 p-4 rounded-xl border bg-card">
34
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0">
35
+ <Phone className="w-5 h-5" suppressHydrationWarning />
36
+ </div>
37
+ <div>
38
+ <h3 className="font-semibold text-sm">Phone</h3>
39
+ <p className="text-sm text-muted-foreground">+91 98765 43210</p>
40
+ </div>
41
+ </div>
42
+ <div className="flex items-start gap-4 p-4 rounded-xl border bg-card">
43
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0">
44
+ <MapPin className="w-5 h-5" suppressHydrationWarning />
45
+ </div>
46
+ <div>
47
+ <h3 className="font-semibold text-sm">Location</h3>
48
+ <p className="text-sm text-muted-foreground">Hyderabad, India</p>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ {/* Contact form */}
54
+ <form
55
+ className="animate-fade-in-up animate-delay-3 lg:col-span-3 flex flex-col gap-5 p-8 rounded-2xl border bg-card"
56
+ onSubmit={(e) => e.preventDefault()}
57
+ >
58
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
59
+ <div className="flex flex-col gap-1.5">
60
+ <label className="text-sm font-medium" htmlFor="name">Name</label>
61
+ <input id="name" placeholder="John Doe" className="h-10 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" />
62
+ </div>
63
+ <div className="flex flex-col gap-1.5">
64
+ <label className="text-sm font-medium" htmlFor="email">Email</label>
65
+ <input id="email" type="email" placeholder="john@example.com" className="h-10 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" />
66
+ </div>
67
+ </div>
68
+ <div className="flex flex-col gap-1.5">
69
+ <label className="text-sm font-medium" htmlFor="subject">Subject</label>
70
+ <input id="subject" placeholder="Project inquiry" className="h-10 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50" />
71
+ </div>
72
+ <div className="flex flex-col gap-1.5">
73
+ <label className="text-sm font-medium" htmlFor="message">Message</label>
74
+ <textarea id="message" rows={5} placeholder="Tell us about your project..." className="rounded-md border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none" />
75
+ </div>
76
+ <Button type="submit" size="lg" className="self-start group">
77
+ Send Message
78
+ <Send className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" suppressHydrationWarning />
79
+ </Button>
80
+ </form>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ )
85
+ }
app/(public)/layout.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Header } from "@/components/layout/Header"
2
+ import { Footer } from "@/components/layout/Footer"
3
+
4
+ export default function PublicLayout({
5
+ children,
6
+ }: {
7
+ children: React.ReactNode
8
+ }) {
9
+ return (
10
+ <div className="flex min-h-screen flex-col">
11
+ <Header />
12
+ <main className="flex-1">{children}</main>
13
+ <Footer />
14
+ </div>
15
+ )
16
+ }
app/(public)/page.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HeroSection } from "@/components/home/HeroSection";
2
+ import { TrustIndicators } from "@/components/home/TrustIndicators";
3
+ import { ServicesOverview } from "@/components/home/ServicesOverview";
4
+ import { FeaturedPortfolio } from "@/components/home/FeaturedPortfolio";
5
+ import { TestimonialsCarousel } from "@/components/home/TestimonialsCarousel";
6
+ import { CTASection } from "@/components/home/CTASection";
7
+
8
+ export default function Home() {
9
+ return (
10
+ <>
11
+ <HeroSection />
12
+ <TrustIndicators />
13
+ <ServicesOverview />
14
+ <FeaturedPortfolio />
15
+ <TestimonialsCarousel />
16
+ <CTASection />
17
+ </>
18
+ );
19
+ }
app/(public)/portfolio/[slug]/page.tsx ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { notFound } from "next/navigation"
2
+ import Link from "next/link"
3
+ import { Button } from "@/components/ui/button"
4
+ import { Card, CardContent, CardFooter } from "@/components/ui/card"
5
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
6
+ import { ArrowLeft, Star, ExternalLink } from "lucide-react"
7
+ import { projects } from "@/lib/portfolio-data"
8
+
9
+ export function generateStaticParams() {
10
+ return projects.map((p) => ({ slug: p.slug }))
11
+ }
12
+
13
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
14
+ const { slug } = await params
15
+ const project = projects.find((p) => p.slug === slug)
16
+ if (!project) return {}
17
+ return {
18
+ title: `${project.title} — NexaFlow Portfolio`,
19
+ description: project.description,
20
+ }
21
+ }
22
+
23
+ export default async function CaseStudyPage({ params }: { params: Promise<{ slug: string }> }) {
24
+ const { slug } = await params
25
+ const project = projects.find((p) => p.slug === slug)
26
+ if (!project) notFound()
27
+
28
+ return (
29
+ <article className="pb-24">
30
+ {/* Hero banner */}
31
+ <div className={`relative bg-gradient-to-br ${project.gradient} py-20 md:py-28`}>
32
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(255,255,255,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.05)_1px,transparent_1px)] bg-[size:3rem_3rem]" />
33
+ <div className="container relative z-10">
34
+ <Button asChild variant="outline" size="sm" className="mb-8 bg-white/10 border-white/20 text-white hover:bg-white/20">
35
+ <Link href="/portfolio">
36
+ <ArrowLeft className="mr-2 h-4 w-4" suppressHydrationWarning />
37
+ Back to Portfolio
38
+ </Link>
39
+ </Button>
40
+ <p className="text-white/70 text-sm font-semibold uppercase tracking-widest mb-3">{project.category}</p>
41
+ <h1 className="text-white sm:text-5xl lg:text-6xl mb-4">{project.title}</h1>
42
+ <p className="text-white/80 text-lg max-w-2xl leading-relaxed">{project.description}</p>
43
+
44
+ {/* Technology tags (SF-015) */}
45
+ <div className="flex flex-wrap gap-2 mt-6">
46
+ {project.tags.map((tag) => (
47
+ <span key={tag} className="bg-white/15 text-white px-3 py-1 rounded-full text-xs font-medium backdrop-blur-sm">
48
+ {tag}
49
+ </span>
50
+ ))}
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="container mt-16">
56
+ {/* Key results metrics */}
57
+ {project.results && project.results.length > 0 && (
58
+ <div className="animate-fade-in-up grid grid-cols-2 md:grid-cols-4 gap-6 mb-16 -mt-8 relative z-20">
59
+ {project.results.map((r) => (
60
+ <div key={r.metric} className="rounded-2xl border bg-card p-6 text-center shadow-lg">
61
+ <div className="text-2xl font-extrabold text-primary mb-1">{r.value}</div>
62
+ <p className="text-caption">{r.metric}</p>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ )}
67
+
68
+ {/* Case study content */}
69
+ <div className="max-w-3xl mx-auto space-y-12">
70
+ {/* Overview */}
71
+ <section className="animate-fade-in-up">
72
+ <h2 className="mb-4">Overview</h2>
73
+ <p className="text-body text-muted-foreground leading-relaxed">{project.overview}</p>
74
+ </section>
75
+
76
+ {/* Challenge */}
77
+ <section className="animate-fade-in-up animate-delay-1">
78
+ <h2 className="mb-4">The Challenge</h2>
79
+ <div className="rounded-2xl border-l-4 border-destructive bg-destructive/5 p-6">
80
+ <p className="text-body text-muted-foreground leading-relaxed">{project.challenge}</p>
81
+ </div>
82
+ </section>
83
+
84
+ {/* Solution */}
85
+ <section className="animate-fade-in-up animate-delay-2">
86
+ <h2 className="mb-4">Our Solution</h2>
87
+ <div className="rounded-2xl border-l-4 border-primary bg-primary/5 p-6">
88
+ <p className="text-body text-muted-foreground leading-relaxed">{project.solution}</p>
89
+ </div>
90
+ </section>
91
+
92
+ {/* Technologies Used */}
93
+ <section className="animate-fade-in-up animate-delay-3">
94
+ <h2 className="mb-4">Technologies Used</h2>
95
+ <div className="flex flex-wrap gap-3">
96
+ {project.tags.map((tag) => (
97
+ <span key={tag} className="bg-secondary text-secondary-foreground px-4 py-2 rounded-xl text-sm font-medium">
98
+ {tag}
99
+ </span>
100
+ ))}
101
+ </div>
102
+ </section>
103
+
104
+ {/* Client Testimonial (SF-017) */}
105
+ {project.testimonial && (
106
+ <section className="animate-fade-in-up">
107
+ <h2 className="mb-4">Client Feedback</h2>
108
+ <Card className="bg-muted border">
109
+ <CardContent className="pt-6">
110
+ <div className="flex gap-0.5 mb-4" role="img" aria-label={`${project.testimonial.rating} out of 5 stars`}>
111
+ {[...Array(project.testimonial.rating)].map((_, j) => (
112
+ <Star key={j} className="w-4 h-4 fill-amber-400 text-amber-400" suppressHydrationWarning />
113
+ ))}
114
+ </div>
115
+ <blockquote className="text-body text-foreground leading-relaxed">
116
+ &ldquo;{project.testimonial.content}&rdquo;
117
+ </blockquote>
118
+ </CardContent>
119
+ <CardFooter className="flex items-center gap-4 border-t pt-4">
120
+ <Avatar className="h-10 w-10">
121
+ <AvatarFallback className="bg-primary/10 text-primary text-sm font-semibold">
122
+ {project.testimonial.avatar}
123
+ </AvatarFallback>
124
+ </Avatar>
125
+ <div>
126
+ <p className="font-semibold text-sm">{project.testimonial.name}</p>
127
+ <p className="text-caption">{project.testimonial.role}, {project.testimonial.company}</p>
128
+ </div>
129
+ </CardFooter>
130
+ </Card>
131
+ </section>
132
+ )}
133
+ </div>
134
+
135
+ {/* CTA */}
136
+ <div className="animate-fade-in-up text-center mt-16 pt-12 border-t">
137
+ <h2 className="mb-4">Ready to build something like this?</h2>
138
+ <p className="text-body text-muted-foreground mb-8 max-w-lg mx-auto">
139
+ Let&apos;s discuss your project and see how we can help you achieve similar results.
140
+ </p>
141
+ <div className="flex justify-center gap-4">
142
+ <Button asChild size="xl">
143
+ <Link href="/quote">Start Your Project</Link>
144
+ </Button>
145
+ <Button asChild variant="outline" size="lg">
146
+ <Link href="/portfolio">
147
+ <ArrowLeft className="mr-2 h-4 w-4" suppressHydrationWarning />
148
+ Back to Portfolio
149
+ </Link>
150
+ </Button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </article>
155
+ )
156
+ }
app/(public)/portfolio/page.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ProjectGallery, Project } from "@/components/portfolio/ProjectGallery"
2
+
3
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
4
+
5
+ async function getPortfolioItems() {
6
+ try {
7
+ const res = await fetch(`${API_URL}/portfolio`, { cache: 'no-store' })
8
+ if (!res.ok) return []
9
+ const data = await res.json()
10
+
11
+ return data.map((item: any) => ({
12
+ title: item.title,
13
+ slug: item.slug,
14
+ category: item.industry || "General",
15
+ description: item.description,
16
+ tags: item.technologies ? item.technologies.split(',').map((t: string) => t.trim()) : [],
17
+ imageUrl: item.imageUrl,
18
+ gradient: "from-gray-800 to-gray-600",
19
+ emoji: "✨",
20
+ }))
21
+ } catch (e) {
22
+ console.error("Failed to fetch public portfolio items", e)
23
+ return []
24
+ }
25
+ }
26
+
27
+ export default async function PortfolioPage() {
28
+ const projects: Project[] = await getPortfolioItems()
29
+
30
+ return (
31
+ <div className="py-20">
32
+ <div className="container">
33
+ {/* Hero header */}
34
+ <div className="animate-fade-in-up text-center mb-16 max-w-3xl mx-auto">
35
+ <p className="text-overline text-primary mb-3">Our Work</p>
36
+ <h1 className="sm:text-5xl mb-6">
37
+ Projects that{" "}
38
+ <span className="text-primary">speak for themselves</span>
39
+ </h1>
40
+ <p className="text-body text-muted-foreground">
41
+ A curated selection of our best work across web development, mobile apps, and design.
42
+ </p>
43
+ </div>
44
+
45
+ {/* Gallery with filters (SF-013) */}
46
+ <ProjectGallery projects={projects} />
47
+ </div>
48
+ </div>
49
+ )
50
+ }
app/(public)/quote/page.tsx ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useCallback } from "react"
4
+ import Link from "next/link"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Card, CardContent } from "@/components/ui/card"
8
+ import {
9
+ ArrowRight,
10
+ ArrowLeft,
11
+ CheckCircle2,
12
+ Globe,
13
+ Smartphone,
14
+ Palette,
15
+ Search,
16
+ HelpCircle,
17
+ DollarSign,
18
+ Clock,
19
+ Zap,
20
+ CalendarDays,
21
+ Timer,
22
+ Infinity,
23
+ Upload,
24
+ Loader2,
25
+ PartyPopper,
26
+ FileText,
27
+ X,
28
+ } from "lucide-react"
29
+
30
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
31
+
32
+ /* ─── Step data ─── */
33
+
34
+ const projectTypes = [
35
+ { value: "website", label: "Website", desc: "Marketing sites, landing pages, corporate websites", icon: Globe },
36
+ { value: "webapp", label: "Web Application", desc: "SaaS platforms, dashboards, custom tools", icon: Zap },
37
+ { value: "mobile", label: "Mobile App", desc: "iOS, Android, or cross-platform apps", icon: Smartphone },
38
+ { value: "design", label: "UI/UX Design", desc: "User research, wireframes, prototypes", icon: Palette },
39
+ { value: "seo", label: "SEO Optimization", desc: "Audit, strategy, and ongoing optimization", icon: Search },
40
+ { value: "other", label: "Other", desc: "Custom requirements or multiple services", icon: HelpCircle },
41
+ ]
42
+
43
+ const budgetRanges = [
44
+ { value: "$1K–$5K", label: "$1K – $5K", desc: "Small projects, MVPs, single-page sites", icon: DollarSign },
45
+ { value: "$5K–$15K", label: "$5K – $15K", desc: "Multi-page websites, mobile apps", icon: DollarSign },
46
+ { value: "$15K–$50K", label: "$15K – $50K", desc: "Complex platforms, enterprise solutions", icon: DollarSign },
47
+ { value: "$50K+", label: "$50K+", desc: "Large-scale projects, ongoing partnerships", icon: DollarSign },
48
+ ]
49
+
50
+ const timelineOptions = [
51
+ { value: "ASAP", label: "ASAP", desc: "As soon as possible", icon: Zap },
52
+ { value: "1-2 months", label: "1–2 Months", desc: "Standard delivery timeline", icon: Clock },
53
+ { value: "3-6 months", label: "3–6 Months", desc: "Complex or phased rollout", icon: CalendarDays },
54
+ { value: "Flexible", label: "Flexible", desc: "No strict deadline", icon: Infinity },
55
+ ]
56
+
57
+ const estimateMap: Record<string, Record<string, string>> = {
58
+ "$1K–$5K": { website: "$2K–$4K", webapp: "$3K–$5K", mobile: "$4K–$5K", design: "$1K–$3K", seo: "$1K–$3K", other: "$2K–$5K" },
59
+ "$5K–$15K": { website: "$6K–$12K", webapp: "$8K–$15K", mobile: "$8K–$14K", design: "$5K–$10K", seo: "$5K–$8K", other: "$6K–$12K" },
60
+ "$15K–$50K": { website: "$15K–$30K", webapp: "$20K–$45K", mobile: "$18K–$40K", design: "$12K–$25K", seo: "$10K–$20K", other: "$15K–$35K" },
61
+ "$50K+": { website: "$50K–$80K", webapp: "$50K–$120K", mobile: "$50K–$100K", design: "$30K–$60K", seo: "$25K–$50K", other: "$50K+" },
62
+ }
63
+
64
+ type FormState = {
65
+ projectType: string
66
+ description: string
67
+ budget: string
68
+ timeline: string
69
+ name: string
70
+ email: string
71
+ phone: string
72
+ company: string
73
+ files: File[]
74
+ }
75
+
76
+ const initialState: FormState = {
77
+ projectType: "",
78
+ description: "",
79
+ budget: "",
80
+ timeline: "",
81
+ name: "",
82
+ email: "",
83
+ phone: "",
84
+ company: "",
85
+ files: [],
86
+ }
87
+
88
+ const TOTAL_STEPS = 4
89
+
90
+ export default function QuotePage() {
91
+ const [step, setStep] = useState(1)
92
+ const [form, setForm] = useState<FormState>(initialState)
93
+ const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
94
+ const [submitting, setSubmitting] = useState(false)
95
+ const [submitted, setSubmitted] = useState(false)
96
+ const [submitError, setSubmitError] = useState("")
97
+ const [inquiryId, setInquiryId] = useState("")
98
+
99
+ const set = useCallback(<K extends keyof FormState>(key: K, value: FormState[K]) => {
100
+ setForm((prev) => ({ ...prev, [key]: value }))
101
+ setErrors((prev) => ({ ...prev, [key]: undefined }))
102
+ }, [])
103
+
104
+ /* ─── Validation ─── */
105
+ const validateStep = (): boolean => {
106
+ const errs: typeof errors = {}
107
+ if (step === 1) {
108
+ if (!form.projectType) errs.projectType = "Please select a project type"
109
+ if (form.description.trim().length < 10) errs.description = "Please describe your project (min 10 characters)"
110
+ }
111
+ if (step === 2) {
112
+ if (!form.budget) errs.budget = "Please select a budget range"
113
+ }
114
+ if (step === 3) {
115
+ if (!form.timeline) errs.timeline = "Please select a timeline"
116
+ }
117
+ if (step === 4) {
118
+ if (form.name.trim().length < 2) errs.name = "Name is required"
119
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errs.email = "A valid email is required"
120
+ }
121
+ setErrors(errs)
122
+ return Object.keys(errs).length === 0
123
+ }
124
+
125
+ const next = () => { if (validateStep()) setStep((s) => Math.min(s + 1, TOTAL_STEPS)) }
126
+ const back = () => setStep((s) => Math.max(s - 1, 1))
127
+
128
+ /* ─── File handling ─── */
129
+ const ACCEPTED = [".pdf", ".doc", ".docx", ".png", ".jpg", ".jpeg"]
130
+ const MAX_SIZE = 10 * 1024 * 1024 // 10 MB
131
+ const MAX_FILES = 3
132
+
133
+ const addFiles = (fileList: FileList | null) => {
134
+ if (!fileList) return
135
+ const newFiles = Array.from(fileList).filter((f) => {
136
+ const ext = "." + f.name.split(".").pop()?.toLowerCase()
137
+ if (!ACCEPTED.includes(ext)) return false
138
+ if (f.size > MAX_SIZE) return false
139
+ return true
140
+ })
141
+ set("files", [...form.files, ...newFiles].slice(0, MAX_FILES))
142
+ }
143
+ const removeFile = (idx: number) => set("files", form.files.filter((_, i) => i !== idx))
144
+
145
+ /* ─── Submit ─── */
146
+ const handleSubmit = async () => {
147
+ if (!validateStep()) return
148
+ setSubmitting(true)
149
+ setSubmitError("")
150
+ try {
151
+ // First fetch the CSRF token
152
+ const csrfRes = await fetch(`${API_URL}/csrf-token`, { method: "GET" })
153
+ if (!csrfRes.ok) throw new Error("Could not initialize secure session")
154
+ const { csrfToken } = await csrfRes.json()
155
+
156
+ const res = await fetch(`${API_URL}/inquiries`, {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ "X-CSRF-Token": csrfToken
161
+ },
162
+ body: JSON.stringify({
163
+ name: form.name.trim(),
164
+ email: form.email.trim().toLowerCase(),
165
+ phone: form.phone.trim() || null,
166
+ company: form.company.trim() || null,
167
+ projectType: form.projectType,
168
+ budget: form.budget,
169
+ timeline: form.timeline,
170
+ description: form.description.trim(),
171
+ }),
172
+ })
173
+ const data = await res.json()
174
+ if (!res.ok) {
175
+ setSubmitError(data.error || "Something went wrong")
176
+ return
177
+ }
178
+ setInquiryId(data.id)
179
+ setSubmitted(true)
180
+ } catch (err: any) {
181
+ setSubmitError(err.message || "Network error. Please try again.")
182
+ } finally {
183
+ setSubmitting(false)
184
+ }
185
+ }
186
+
187
+ /* ─── Estimate ─── */
188
+ const estimate = form.budget && form.projectType
189
+ ? estimateMap[form.budget]?.[form.projectType] || null
190
+ : null
191
+
192
+ /* ─── Success screen ─── */
193
+ if (submitted) {
194
+ return (
195
+ <div className="py-20">
196
+ <div className="container max-w-2xl mx-auto text-center animate-fade-in-up">
197
+ <div className="w-20 h-20 mx-auto rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-6">
198
+ <PartyPopper className="h-10 w-10 text-green-600 dark:text-green-400" suppressHydrationWarning />
199
+ </div>
200
+ <h1 className="text-3xl font-bold mb-3">Quote Request Submitted!</h1>
201
+ <p className="text-muted-foreground mb-2">
202
+ Your reference number is <span className="font-mono font-semibold text-foreground">{inquiryId}</span>
203
+ </p>
204
+ <p className="text-muted-foreground mb-8">
205
+ We&apos;ll review your request and get back to you within 24 hours.
206
+ </p>
207
+
208
+ {estimate && (
209
+ <Card className="mb-8 border-primary/20 bg-primary/5">
210
+ <CardContent className="py-6">
211
+ <p className="text-sm text-muted-foreground mb-1">Estimated project range</p>
212
+ <p className="text-3xl font-extrabold text-primary">{estimate}</p>
213
+ <p className="text-xs text-muted-foreground mt-2">
214
+ This is a rough estimate — your actual quote may vary based on project details.
215
+ </p>
216
+ </CardContent>
217
+ </Card>
218
+ )}
219
+
220
+ <div className="flex justify-center gap-4">
221
+ <Button asChild size="lg">
222
+ <Link href="/contact">Book a Consultation</Link>
223
+ </Button>
224
+ <Button asChild variant="outline" size="lg">
225
+ <Link href="/">Back to Home</Link>
226
+ </Button>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ )
231
+ }
232
+
233
+ /* ─── Form steps ─── */
234
+ return (
235
+ <div className="py-20">
236
+ <div className="container max-w-3xl mx-auto">
237
+ {/* Header */}
238
+ <div className="text-center mb-10 animate-fade-in-up">
239
+ <p className="text-sm font-semibold text-primary uppercase tracking-[0.15em] mb-3">Get a Quote</p>
240
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl mb-4">
241
+ Let&apos;s bring your <span className="text-primary">idea to life</span>
242
+ </h1>
243
+ <p className="text-lg text-muted-foreground max-w-xl mx-auto">
244
+ Answer a few questions and we&apos;ll send you a detailed proposal.
245
+ </p>
246
+ </div>
247
+
248
+ {/* Progress indicator */}
249
+ <div className="flex items-center justify-center gap-2 mb-10" role="progressbar" aria-valuenow={step} aria-valuemin={1} aria-valuemax={TOTAL_STEPS}>
250
+ {[1, 2, 3, 4].map((s) => (
251
+ <div key={s} className="flex items-center gap-2">
252
+ <div
253
+ className={`w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold transition-all duration-300 ${s < step
254
+ ? "bg-primary text-primary-foreground"
255
+ : s === step
256
+ ? "bg-primary text-primary-foreground ring-4 ring-primary/20"
257
+ : "bg-muted text-muted-foreground"
258
+ }`}
259
+ >
260
+ {s < step ? <CheckCircle2 className="h-5 w-5" suppressHydrationWarning /> : s}
261
+ </div>
262
+ {s < 4 && (
263
+ <div className={`w-12 h-0.5 transition-colors duration-300 ${s < step ? "bg-primary" : "bg-muted"}`} />
264
+ )}
265
+ </div>
266
+ ))}
267
+ </div>
268
+
269
+ {/* Step card */}
270
+ <Card className="animate-fade-in-up border shadow-lg">
271
+ <CardContent className="p-8">
272
+ {/* ─── STEP 1: Project Details ─── */}
273
+ {step === 1 && (
274
+ <fieldset className="space-y-6">
275
+ <legend className="text-xl font-bold mb-1">What are you looking for?</legend>
276
+ <p className="text-sm text-muted-foreground">Select the type of project and describe your vision.</p>
277
+
278
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
279
+ {projectTypes.map((pt) => (
280
+ <button
281
+ key={pt.value}
282
+ type="button"
283
+ onClick={() => set("projectType", pt.value)}
284
+ className={`flex flex-col items-center gap-2 p-4 rounded-xl border text-center transition-all duration-200 hover:border-primary/40 hover:shadow-md ${form.projectType === pt.value
285
+ ? "border-primary bg-primary/5 ring-2 ring-primary/20 shadow-md"
286
+ : "border-border"
287
+ }`}
288
+ >
289
+ <pt.icon className={`h-6 w-6 ${form.projectType === pt.value ? "text-primary" : "text-muted-foreground"}`} suppressHydrationWarning />
290
+ <span className="font-medium text-sm">{pt.label}</span>
291
+ <span className="text-xs text-muted-foreground leading-tight hidden sm:block">{pt.desc}</span>
292
+ </button>
293
+ ))}
294
+ </div>
295
+ {errors.projectType && <p className="text-sm text-destructive">{errors.projectType}</p>}
296
+
297
+ <div>
298
+ <label className="text-sm font-medium mb-1.5 block" htmlFor="description">
299
+ Tell us about your project *
300
+ </label>
301
+ <textarea
302
+ id="description"
303
+ className="flex min-h-[100px] w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 resize-none"
304
+ value={form.description}
305
+ onChange={(e) => set("description", e.target.value)}
306
+ placeholder="Describe your goals, target audience, key features, and any inspiration..."
307
+ maxLength={2000}
308
+ />
309
+ <div className="flex justify-between mt-1">
310
+ {errors.description ? (
311
+ <p className="text-sm text-destructive">{errors.description}</p>
312
+ ) : <span />}
313
+ <span className="text-xs text-muted-foreground">{form.description.length}/2000</span>
314
+ </div>
315
+ </div>
316
+ </fieldset>
317
+ )}
318
+
319
+ {/* ─── STEP 2: Budget ─── */}
320
+ {step === 2 && (
321
+ <fieldset className="space-y-6">
322
+ <legend className="text-xl font-bold mb-1">What&apos;s your budget?</legend>
323
+ <p className="text-sm text-muted-foreground">Select the range that best fits your project investment.</p>
324
+
325
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
326
+ {budgetRanges.map((b, i) => (
327
+ <button
328
+ key={b.value}
329
+ type="button"
330
+ onClick={() => set("budget", b.value)}
331
+ className={`flex items-center gap-4 p-5 rounded-xl border text-left transition-all duration-200 hover:border-primary/40 hover:shadow-md ${form.budget === b.value
332
+ ? "border-primary bg-primary/5 ring-2 ring-primary/20 shadow-md"
333
+ : "border-border"
334
+ }`}
335
+ >
336
+ <div className={`flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center text-sm font-bold ${form.budget === b.value
337
+ ? "bg-primary text-primary-foreground"
338
+ : "bg-muted text-muted-foreground"
339
+ }`}>
340
+ {"$".repeat(i + 1)}
341
+ </div>
342
+ <div>
343
+ <p className="font-semibold">{b.label}</p>
344
+ <p className="text-xs text-muted-foreground">{b.desc}</p>
345
+ </div>
346
+ </button>
347
+ ))}
348
+ </div>
349
+ {errors.budget && <p className="text-sm text-destructive">{errors.budget}</p>}
350
+ </fieldset>
351
+ )}
352
+
353
+ {/* ─── STEP 3: Timeline ─── */}
354
+ {step === 3 && (
355
+ <fieldset className="space-y-6">
356
+ <legend className="text-xl font-bold mb-1">When do you need it?</legend>
357
+ <p className="text-sm text-muted-foreground">Select your preferred timeline.</p>
358
+
359
+ <div className="grid grid-cols-2 gap-4">
360
+ {timelineOptions.map((t) => (
361
+ <button
362
+ key={t.value}
363
+ type="button"
364
+ onClick={() => set("timeline", t.value)}
365
+ className={`flex items-center gap-4 p-5 rounded-xl border text-left transition-all duration-200 hover:border-primary/40 hover:shadow-md ${form.timeline === t.value
366
+ ? "border-primary bg-primary/5 ring-2 ring-primary/20 shadow-md"
367
+ : "border-border"
368
+ }`}
369
+ >
370
+ <div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${form.timeline === t.value
371
+ ? "bg-primary text-primary-foreground"
372
+ : "bg-muted text-muted-foreground"
373
+ }`}>
374
+ <t.icon className="h-5 w-5" suppressHydrationWarning />
375
+ </div>
376
+ <div>
377
+ <p className="font-semibold">{t.label}</p>
378
+ <p className="text-xs text-muted-foreground">{t.desc}</p>
379
+ </div>
380
+ </button>
381
+ ))}
382
+ </div>
383
+ {errors.timeline && <p className="text-sm text-destructive">{errors.timeline}</p>}
384
+ </fieldset>
385
+ )}
386
+
387
+ {/* ─── STEP 4: Contact & Upload ─── */}
388
+ {step === 4 && (
389
+ <fieldset className="space-y-5">
390
+ <legend className="text-xl font-bold mb-1">Almost there!</legend>
391
+ <p className="text-sm text-muted-foreground">How can we reach you?</p>
392
+
393
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
394
+ <div>
395
+ <label className="text-sm font-medium mb-1.5 block" htmlFor="name">Full Name *</label>
396
+ <Input
397
+ id="name"
398
+ value={form.name}
399
+ onChange={(e) => set("name", e.target.value)}
400
+ placeholder="John Doe"
401
+ />
402
+ {errors.name && <p className="text-sm text-destructive mt-1">{errors.name}</p>}
403
+ </div>
404
+ <div>
405
+ <label className="text-sm font-medium mb-1.5 block" htmlFor="email">Email *</label>
406
+ <Input
407
+ id="email"
408
+ type="email"
409
+ value={form.email}
410
+ onChange={(e) => set("email", e.target.value)}
411
+ placeholder="john@company.com"
412
+ />
413
+ {errors.email && <p className="text-sm text-destructive mt-1">{errors.email}</p>}
414
+ </div>
415
+ </div>
416
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
417
+ <div>
418
+ <label className="text-sm font-medium mb-1.5 block" htmlFor="phone">Phone (optional)</label>
419
+ <Input
420
+ id="phone"
421
+ type="tel"
422
+ value={form.phone}
423
+ onChange={(e) => set("phone", e.target.value)}
424
+ placeholder="+1 (555) 000-0000"
425
+ />
426
+ </div>
427
+ <div>
428
+ <label className="text-sm font-medium mb-1.5 block" htmlFor="company">Company (optional)</label>
429
+ <Input
430
+ id="company"
431
+ value={form.company}
432
+ onChange={(e) => set("company", e.target.value)}
433
+ placeholder="Your Company"
434
+ />
435
+ </div>
436
+ </div>
437
+
438
+ {/* File upload */}
439
+ <div>
440
+ <label className="text-sm font-medium mb-1.5 block">Attachments (optional)</label>
441
+ <div
442
+ className="border-2 border-dashed rounded-xl p-6 text-center transition-colors hover:border-primary/40 cursor-pointer"
443
+ onDragOver={(e) => e.preventDefault()}
444
+ onDrop={(e) => { e.preventDefault(); addFiles(e.dataTransfer.files) }}
445
+ onClick={() => document.getElementById("file-input")?.click()}
446
+ role="button"
447
+ tabIndex={0}
448
+ onKeyDown={(e) => { if (e.key === "Enter") document.getElementById("file-input")?.click() }}
449
+ >
450
+ <Upload className="h-8 w-8 mx-auto text-muted-foreground mb-2" suppressHydrationWarning />
451
+ <p className="text-sm text-muted-foreground">
452
+ Drag & drop or <span className="text-primary font-medium">browse files</span>
453
+ </p>
454
+ <p className="text-xs text-muted-foreground mt-1">
455
+ PDF, DOC, PNG, JPG — max 10 MB, up to 3 files
456
+ </p>
457
+ <input
458
+ id="file-input"
459
+ type="file"
460
+ className="hidden"
461
+ multiple
462
+ accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"
463
+ onChange={(e) => addFiles(e.target.files)}
464
+ />
465
+ </div>
466
+ {form.files.length > 0 && (
467
+ <div className="mt-3 space-y-2">
468
+ {form.files.map((file, idx) => (
469
+ <div key={idx} className="flex items-center gap-3 bg-muted/50 rounded-lg px-3 py-2 text-sm">
470
+ <FileText className="h-4 w-4 text-muted-foreground shrink-0" suppressHydrationWarning />
471
+ <span className="flex-1 truncate">{file.name}</span>
472
+ <span className="text-xs text-muted-foreground">{(file.size / 1024).toFixed(0)} KB</span>
473
+ <button type="button" onClick={() => removeFile(idx)} className="text-muted-foreground hover:text-destructive">
474
+ <X className="h-4 w-4" suppressHydrationWarning />
475
+ </button>
476
+ </div>
477
+ ))}
478
+ </div>
479
+ )}
480
+ </div>
481
+ </fieldset>
482
+ )}
483
+
484
+ {/* Submit error */}
485
+ {submitError && <p className="text-sm text-destructive mt-4">{submitError}</p>}
486
+
487
+ {/* Navigation buttons */}
488
+ <div className="flex justify-between items-center mt-8 pt-6 border-t">
489
+ {step > 1 ? (
490
+ <Button type="button" variant="outline" onClick={back}>
491
+ <ArrowLeft className="mr-2 h-4 w-4" suppressHydrationWarning />
492
+ Back
493
+ </Button>
494
+ ) : <span />}
495
+
496
+ {step < TOTAL_STEPS ? (
497
+ <Button type="button" onClick={next}>
498
+ Next
499
+ <ArrowRight className="ml-2 h-4 w-4" suppressHydrationWarning />
500
+ </Button>
501
+ ) : (
502
+ <Button type="button" onClick={handleSubmit} disabled={submitting} size="lg">
503
+ {submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" suppressHydrationWarning />}
504
+ Submit Quote Request
505
+ {!submitting && <ArrowRight className="ml-2 h-4 w-4" suppressHydrationWarning />}
506
+ </Button>
507
+ )}
508
+ </div>
509
+ </CardContent>
510
+ </Card>
511
+
512
+ {/* Trust signals */}
513
+ <div className="flex flex-wrap justify-center gap-6 mt-8 text-sm text-muted-foreground">
514
+ {["Free consultation", "No hidden fees", "Response within 24h", "100% confidential"].map((t) => (
515
+ <span key={t} className="flex items-center gap-1.5">
516
+ <CheckCircle2 className="h-4 w-4 text-green-500" suppressHydrationWarning />
517
+ {t}
518
+ </span>
519
+ ))}
520
+ </div>
521
+ </div>
522
+ </div>
523
+ )
524
+ }
app/(public)/services/page.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PackageComparison } from "@/components/services/PackageComparison"
2
+ import { ServiceDescriptions } from "@/components/services/ServiceDescriptions"
3
+ import { ProcessTimeline } from "@/components/services/ProcessTimeline"
4
+ import { FAQSection } from "@/components/services/FAQSection"
5
+ import { AddOnServices } from "@/components/services/AddOnServices"
6
+
7
+ export default function ServicesPage() {
8
+ return (
9
+ <>
10
+ {/* Hero header */}
11
+ <section className="pt-20 pb-10">
12
+ <div className="container">
13
+ <div className="animate-fade-in-up text-center max-w-3xl mx-auto">
14
+ <p className="text-overline text-primary mb-3">Our Services</p>
15
+ <h1 className="sm:text-5xl mb-6">
16
+ Everything you need to{" "}
17
+ <span className="text-primary">grow online</span>
18
+ </h1>
19
+ <p className="text-body text-muted-foreground">
20
+ We provide end-to-end digital solutions — from strategy and design to development and optimization.
21
+ </p>
22
+ </div>
23
+ </div>
24
+ </section>
25
+
26
+ <ServiceDescriptions />
27
+ <PackageComparison />
28
+ <ProcessTimeline />
29
+ <AddOnServices />
30
+ <FAQSection />
31
+ </>
32
+ )
33
+ }
app/admin/blog/[id]/page.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, use } from 'react';
4
+ import { notFound } from 'next/navigation';
5
+ import BlogForm from '@/components/admin/BlogForm';
6
+ import { Loader2 } from 'lucide-react';
7
+ import { RoleGuard } from "@/components/auth/RoleGuard";
8
+ import { Role } from "@/types/auth";
9
+ import { useSession } from "next-auth/react";
10
+
11
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
12
+
13
+ export default function EditBlogPostPage({ params }: { params: Promise<{ id: string }> }) {
14
+ const { id } = use(params);
15
+ const { data: session } = useSession();
16
+ const [post, setPost] = useState<any>(null);
17
+ const [isLoading, setIsLoading] = useState(true);
18
+ const [error, setError] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (!session?.user?.id) return;
22
+
23
+ const fetchPost = async () => {
24
+ try {
25
+ const res = await fetch(`${API_URL}/admin/blog/${id}`, {
26
+ headers: {
27
+ 'Authorization': `Bearer ${session?.user?.id}`
28
+ }
29
+ });
30
+ if (res.ok) {
31
+ const data = await res.json();
32
+ setPost(data);
33
+ } else {
34
+ setError(true);
35
+ }
36
+ } catch (err) {
37
+ console.error("Error fetching admin post:", err);
38
+ setError(true);
39
+ } finally {
40
+ setIsLoading(false);
41
+ }
42
+ };
43
+
44
+ fetchPost();
45
+ }, [id, session]);
46
+
47
+ if (isLoading) {
48
+ return (
49
+ <div className="h-[60vh] flex items-center justify-center">
50
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ if (error || !post) {
56
+ notFound();
57
+ }
58
+
59
+ return (
60
+ <RoleGuard allowedRoles={[Role.SUPER_ADMIN, Role.EDITOR]}>
61
+ <div className="max-w-5xl mx-auto py-6">
62
+ <BlogForm initialData={post} isEditing />
63
+ </div>
64
+ </RoleGuard>
65
+ );
66
+ }
app/admin/blog/new/page.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import BlogForm from '@/components/admin/BlogForm';
2
+ import { RoleGuard } from "@/components/auth/RoleGuard";
3
+ import { Role } from "@/types/auth";
4
+
5
+ export const metadata = {
6
+ title: 'New Blog Post | NexaFlow Admin',
7
+ };
8
+
9
+ export default function NewBlogPostPage() {
10
+ return (
11
+ <RoleGuard allowedRoles={[Role.SUPER_ADMIN, Role.EDITOR]}>
12
+ <div className="max-w-5xl mx-auto py-6">
13
+ <BlogForm />
14
+ </div>
15
+ </RoleGuard>
16
+ );
17
+ }
app/admin/blog/page.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Link from 'next/link';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
8
+ import { Plus, Search, Pencil, Trash2, Globe, FileText, CalendarClock } from 'lucide-react';
9
+ import { format } from 'date-fns';
10
+ import { useSession } from "next-auth/react";
11
+ import { RoleGuard } from "@/components/auth/RoleGuard";
12
+ import { Role } from "@/types/auth";
13
+
14
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
15
+
16
+ export default function AdminBlogPage() {
17
+ const { data: session } = useSession();
18
+ const [posts, setPosts] = useState<any[]>([]);
19
+ const [isLoading, setIsLoading] = useState(true);
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+
22
+ useEffect(() => {
23
+ if (session) {
24
+ fetchPosts();
25
+ }
26
+ }, [session]);
27
+
28
+ const fetchPosts = async () => {
29
+ setIsLoading(true);
30
+ try {
31
+ // Using standard fetch since this is an admin dashboard.
32
+ // The Next.js API route will attach the Bearer token or session cookie appropriately.
33
+ const res = await fetch(`${API_URL}/admin/blog`, {
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'Authorization': `Bearer ${session?.user?.id}`
37
+ }
38
+ });
39
+ if (res.ok) {
40
+ const data = await res.json();
41
+ setPosts(data);
42
+ } else {
43
+ console.error("Failed to fetch admin posts");
44
+ }
45
+ } catch (error) {
46
+ console.error("Error fetching posts:", error);
47
+ } finally {
48
+ setIsLoading(false);
49
+ }
50
+ };
51
+
52
+ const handleDelete = async (id: string, title: string) => {
53
+ if (!confirm(`Are you sure you want to delete "${title}"?`)) return;
54
+
55
+ try {
56
+ const res = await fetch(`${API_URL}/admin/blog/${id}`, {
57
+ method: 'DELETE',
58
+ headers: {
59
+ 'Authorization': `Bearer ${session?.user?.id}`
60
+ }
61
+ });
62
+ if (res.ok) {
63
+ setPosts(posts.filter(post => post.id !== id));
64
+ } else {
65
+ alert("Failed to delete post");
66
+ }
67
+ } catch (error) {
68
+ console.error("Error deleting post:", error);
69
+ alert("Error deleting post");
70
+ }
71
+ };
72
+
73
+ const filteredPosts = posts.filter(post =>
74
+ post.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
75
+ post.status.toLowerCase().includes(searchTerm.toLowerCase())
76
+ );
77
+
78
+ const getStatusIcon = (status: string) => {
79
+ switch (status) {
80
+ case 'PUBLISHED': return <Globe className="h-4 w-4 text-green-500" />;
81
+ case 'SCHEDULED': return <CalendarClock className="h-4 w-4 text-blue-500" />;
82
+ case 'DRAFT': return <FileText className="h-4 w-4 text-gray-500" />;
83
+ default: return null;
84
+ }
85
+ };
86
+
87
+ return (
88
+ <RoleGuard allowedRoles={[Role.SUPER_ADMIN, Role.EDITOR]}>
89
+ <div className="space-y-6">
90
+ <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
91
+ <div>
92
+ <h2 className="text-3xl font-bold tracking-tight">Blog Articles</h2>
93
+ <p className="text-muted-foreground">Manage your content, drafts, and scheduled posts.</p>
94
+ </div>
95
+ <Button asChild>
96
+ <Link href="/admin/blog/new">
97
+ <Plus className="mr-2 h-4 w-4" /> New Article
98
+ </Link>
99
+ </Button>
100
+ </div>
101
+
102
+ <Card>
103
+ <CardHeader className="pb-3 border-b border-border/40">
104
+ <div className="flex items-center justify-between">
105
+ <CardTitle>All Posts</CardTitle>
106
+ <div className="relative w-64">
107
+ <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
108
+ <Input
109
+ type="search"
110
+ placeholder="Search articles..."
111
+ className="pl-9 h-9"
112
+ value={searchTerm}
113
+ onChange={(e) => setSearchTerm(e.target.value)}
114
+ />
115
+ </div>
116
+ </div>
117
+ </CardHeader>
118
+ <CardContent className="p-0">
119
+ {isLoading ? (
120
+ <div className="p-8 text-center text-muted-foreground">Loading posts...</div>
121
+ ) : filteredPosts.length === 0 ? (
122
+ <div className="p-8 text-center text-muted-foreground">No posts found matching the criteria.</div>
123
+ ) : (
124
+ <div className="overflow-x-auto">
125
+ <table className="w-full text-sm text-left">
126
+ <thead className="text-xs text-muted-foreground uppercase bg-muted/50 border-b">
127
+ <tr>
128
+ <th className="px-6 py-3 font-medium">Title</th>
129
+ <th className="px-6 py-3 font-medium">Status</th>
130
+ <th className="px-6 py-3 font-medium">Author</th>
131
+ <th className="px-6 py-3 font-medium">Date</th>
132
+ <th className="px-6 py-3 font-medium text-right">Actions</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody>
136
+ {filteredPosts.map((post) => (
137
+ <tr key={post.id} className="border-b last:border-0 hover:bg-muted/30 transition-colors">
138
+ <td className="px-6 py-4">
139
+ <div className="font-medium text-foreground">{post.title}</div>
140
+ <div className="text-xs text-muted-foreground truncate max-w-xs">{post.slug}</div>
141
+ </td>
142
+ <td className="px-6 py-4">
143
+ <div className="flex items-center gap-1.5 text-xs font-medium">
144
+ {getStatusIcon(post.status)}
145
+ <span>{post.status}</span>
146
+ </div>
147
+ </td>
148
+ <td className="px-6 py-4 text-muted-foreground">
149
+ {post.author?.name || 'Unknown'}
150
+ </td>
151
+ <td className="px-6 py-4 text-muted-foreground">
152
+ {post.publishedAt ? format(new Date(post.publishedAt), 'MMM d, yyyy') : 'Not set'}
153
+ </td>
154
+ <td className="px-6 py-4 text-right">
155
+ <div className="flex justify-end gap-2">
156
+ <Button variant="ghost" size="icon" asChild>
157
+ <Link href={`/admin/blog/${post.id}`}>
158
+ <Pencil className="h-4 w-4" />
159
+ <span className="sr-only">Edit</span>
160
+ </Link>
161
+ </Button>
162
+ <Button variant="ghost" size="icon" className="text-destructive hover:bg-destructive/10 hover:text-destructive" onClick={() => handleDelete(post.id, post.title)}>
163
+ <Trash2 className="h-4 w-4" />
164
+ <span className="sr-only">Delete</span>
165
+ </Button>
166
+ </div>
167
+ </td>
168
+ </tr>
169
+ ))}
170
+ </tbody>
171
+ </table>
172
+ </div>
173
+ )}
174
+ </CardContent>
175
+ </Card>
176
+ </div>
177
+ </RoleGuard>
178
+ );
179
+ }
app/admin/dashboard/page.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { auth, signOut } from "@/lib/auth"
2
+ import { redirect } from "next/navigation"
3
+ import { Button } from "@/components/ui/button"
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
5
+ import { MessageSquare, FolderGit2, Users } from "lucide-react"
6
+ import Link from "next/link"
7
+
8
+ export default async function DashboardPage() {
9
+ const session = await auth()
10
+
11
+ if (!session?.user) {
12
+ redirect("/admin/login")
13
+ }
14
+
15
+ return (
16
+ <div className="p-8 space-y-8 max-w-7xl mx-auto">
17
+ <div className="flex items-center justify-between">
18
+ <h1 className="text-3xl font-bold">Admin Dashboard</h1>
19
+ <form
20
+ action={async () => {
21
+ "use server"
22
+ await signOut()
23
+ }}
24
+ >
25
+ <Button variant="outline">Sign Out</Button>
26
+ </form>
27
+ </div>
28
+
29
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
30
+ <Card>
31
+ <CardHeader>
32
+ <CardTitle>Welcome back, {session.user.name || session.user.email}</CardTitle>
33
+ </CardHeader>
34
+ <CardContent>
35
+ <p className="text-sm text-gray-500">
36
+ Role: <span className="font-mono">{session.user.role}</span>
37
+ </p>
38
+ </CardContent>
39
+ </Card>
40
+ </div>
41
+
42
+ <h2 className="text-2xl font-bold mt-12 mb-6">Quick Links</h2>
43
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
44
+ <Link href="/admin/inquiries">
45
+ <Card className="hover:border-primary/50 transition-colors h-full flex flex-col items-center justify-center p-6 text-center">
46
+ <MessageSquare className="h-12 w-12 text-primary mb-4" />
47
+ <CardTitle>Inquiries</CardTitle>
48
+ <CardDescription className="mt-2">Manage quote requests and leads</CardDescription>
49
+ </Card>
50
+ </Link>
51
+
52
+ <Link href="/admin/blog">
53
+ <Card className="hover:border-primary/50 transition-colors h-full flex flex-col items-center justify-center p-6 text-center">
54
+ <FolderGit2 className="h-12 w-12 text-primary mb-4" />
55
+ <CardTitle>Blog Posts</CardTitle>
56
+ <CardDescription className="mt-2">Write, edit, and schedule articles</CardDescription>
57
+ </Card>
58
+ </Link>
59
+
60
+ <Link href="/admin/portfolio">
61
+ <Card className="hover:border-primary/50 transition-colors h-full flex flex-col items-center justify-center p-6 text-center">
62
+ <FolderGit2 className="h-12 w-12 text-primary mb-4" />
63
+ <CardTitle>Portfolio</CardTitle>
64
+ <CardDescription className="mt-2">Manage case studies and projects</CardDescription>
65
+ </Card>
66
+ </Link>
67
+
68
+ <Link href="/admin/testimonials">
69
+ <Card className="hover:border-primary/50 transition-colors h-full flex flex-col items-center justify-center p-6 text-center">
70
+ <Users className="h-12 w-12 text-primary mb-4" />
71
+ <CardTitle>Testimonials</CardTitle>
72
+ <CardDescription className="mt-2">Approve and manage client reviews</CardDescription>
73
+ </Card>
74
+ </Link>
75
+ </div>
76
+ </div>
77
+ )
78
+ }
app/admin/forgot-password/page.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { Label } from "@/components/ui/label"
7
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
8
+ import Link from "next/link"
9
+
10
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
11
+
12
+ export default function ForgotPasswordPage() {
13
+ const [email, setEmail] = useState("")
14
+ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
15
+
16
+ const handleSubmit = async (e: React.FormEvent) => {
17
+ e.preventDefault()
18
+ setStatus("loading")
19
+
20
+ try {
21
+ const res = await fetch(`${API_URL}/auth/forgot-password`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ email }),
25
+ })
26
+
27
+ if (res.ok) {
28
+ setStatus("success")
29
+ } else {
30
+ setStatus("error")
31
+ }
32
+ } catch (error) {
33
+ setStatus("error")
34
+ }
35
+ }
36
+
37
+ return (
38
+ <div className="flex min-h-screen items-center justify-center bg-muted/50 p-4">
39
+ <Card className="w-full max-w-md">
40
+ <CardHeader>
41
+ <CardTitle className="text-2xl font-bold">Forgot Password</CardTitle>
42
+ <CardDescription>
43
+ Enter your email address and we will send you a link to reset your password.
44
+ </CardDescription>
45
+ </CardHeader>
46
+ <CardContent>
47
+ {status === "success" ? (
48
+ <div className="bg-green-100 border border-green-200 text-green-700 px-4 py-3 rounded relative" role="alert">
49
+ <strong className="font-bold">Check your email!</strong>
50
+ <span className="block sm:inline"> We have sent a password reset link to your address.</span>
51
+ </div>
52
+ ) : (
53
+ <form onSubmit={handleSubmit} className="space-y-4">
54
+ <div className="space-y-2">
55
+ <Label htmlFor="email">Email</Label>
56
+ <Input
57
+ id="email"
58
+ type="email"
59
+ placeholder="admin@example.com"
60
+ required
61
+ value={email}
62
+ onChange={(e) => setEmail(e.target.value)}
63
+ disabled={status === "loading"}
64
+ />
65
+ </div>
66
+ <Button type="submit" className="w-full" disabled={status === "loading"}>
67
+ {status === "loading" ? "Sending..." : "Send Reset Link"}
68
+ </Button>
69
+ </form>
70
+ )}
71
+ </CardContent>
72
+ <CardFooter className="flex justify-center">
73
+ <Link href="/admin/login" className="text-sm text-primary hover:underline">
74
+ Back to Login
75
+ </Link>
76
+ </CardFooter>
77
+ </Card>
78
+ </div>
79
+ )
80
+ }
app/admin/inquiries/[id]/page.tsx ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { useSession } from "next-auth/react"
5
+ import { useRouter, useParams } from "next/navigation"
6
+ import { format } from "date-fns"
7
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
8
+ import { Input } from "@/components/ui/input"
9
+ import { Label } from "@/components/ui/label"
10
+ import { Badge } from "@/components/ui/badge"
11
+ import { Button } from "@/components/ui/button"
12
+ import { Textarea } from "@/components/ui/textarea"
13
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
14
+ import { Loader2, ArrowLeft, Mail, Phone, Building, Calendar, DollarSign, Clock, Save } from "lucide-react"
15
+ import Link from "next/link"
16
+ import { toast } from "sonner"
17
+
18
+ interface Inquiry {
19
+ id: string
20
+ name: string
21
+ email: string
22
+ phone: string | null
23
+ company: string | null
24
+ projectType: string | null
25
+ budget: string | null
26
+ timeline: string | null
27
+ description: string
28
+ status: string
29
+ notes: string | null
30
+ createdAt: string
31
+ }
32
+
33
+ const STATUS_OPTIONS = [
34
+ { value: "NEW", label: "New Request" },
35
+ { value: "IN_PROGRESS", label: "In Progress" },
36
+ { value: "QUOTED", label: "Quoted" },
37
+ { value: "WON", label: "Deal Won" },
38
+ { value: "LOST", label: "Closed / Lost" },
39
+ ]
40
+
41
+ export default function InquiryDetailPage() {
42
+ const { data: session } = useSession()
43
+ const router = useRouter()
44
+ const params = useParams()
45
+ const inquiryId = params.id as string
46
+
47
+ const [inquiry, setInquiry] = useState<Inquiry | null>(null)
48
+ const [loading, setLoading] = useState(true)
49
+ const [saving, setSaving] = useState(false)
50
+
51
+ // Form state
52
+ const [status, setStatus] = useState("")
53
+ const [notes, setNotes] = useState("")
54
+
55
+ useEffect(() => {
56
+ if (session && inquiryId) {
57
+ fetchInquiry()
58
+ }
59
+ }, [session, inquiryId])
60
+
61
+ const fetchInquiry = async () => {
62
+ try {
63
+ const res = await fetch(`http://localhost:5000/api/admin/inquiries/${inquiryId}`, {
64
+ headers: {
65
+ Authorization: `Bearer ${session?.user?.id}`,
66
+ },
67
+ })
68
+ if (res.ok) {
69
+ const data = await res.json()
70
+ setInquiry(data)
71
+ setStatus(data.status)
72
+ setNotes(data.notes || "")
73
+ } else {
74
+ toast.error("Inquiry not found")
75
+ router.push("/admin/inquiries")
76
+ }
77
+ } catch (error) {
78
+ console.error("Failed to fetch inquiry:", error)
79
+ toast.error("Failed to load details")
80
+ } finally {
81
+ setLoading(false)
82
+ }
83
+ }
84
+
85
+ const handleSave = async () => {
86
+ setSaving(true)
87
+ try {
88
+ const res = await fetch(`http://localhost:5000/api/admin/inquiries/${inquiryId}`, {
89
+ method: "PATCH",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ Authorization: `Bearer ${session?.user?.id}`,
93
+ },
94
+ body: JSON.stringify({ status, notes }),
95
+ })
96
+
97
+ if (res.ok) {
98
+ toast.success("Inquiry updated successfully")
99
+ const updated = await res.json()
100
+ setInquiry(updated)
101
+ } else {
102
+ toast.error("Failed to update inquiry")
103
+ }
104
+ } catch (error) {
105
+ console.error("Failed to save:", error)
106
+ toast.error("An error occurred")
107
+ } finally {
108
+ setSaving(false)
109
+ }
110
+ }
111
+
112
+ if (loading) {
113
+ return (
114
+ <div className="flex h-[50vh] items-center justify-center">
115
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
116
+ </div>
117
+ )
118
+ }
119
+
120
+ if (!inquiry) return null
121
+
122
+ return (
123
+ <div className="space-y-6 max-w-5xl">
124
+ <div className="flex items-center gap-4">
125
+ <Button variant="outline" size="icon" asChild>
126
+ <Link href="/admin/inquiries">
127
+ <ArrowLeft className="h-4 w-4" />
128
+ </Link>
129
+ </Button>
130
+ <div>
131
+ <h1 className="text-3xl font-bold tracking-tight">Inquiry Details</h1>
132
+ <p className="text-muted-foreground">
133
+ Submitted {format(new Date(inquiry.createdAt), "PPP 'at' p")}
134
+ </p>
135
+ </div>
136
+ </div>
137
+
138
+ <div className="grid gap-6 md:grid-cols-3">
139
+ {/* Main Client Info */}
140
+ <div className="md:col-span-2 space-y-6">
141
+ <Card>
142
+ <CardHeader>
143
+ <CardTitle>Client Information</CardTitle>
144
+ </CardHeader>
145
+ <CardContent className="grid gap-4 sm:grid-cols-2">
146
+ <div className="space-y-1">
147
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
148
+ Name
149
+ </span>
150
+ <p className="font-medium">{inquiry.name}</p>
151
+ </div>
152
+ <div className="space-y-1">
153
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
154
+ <Mail className="h-4 w-4" /> Email
155
+ </span>
156
+ <p className="font-medium">
157
+ <a href={`mailto:${inquiry.email}`} className="text-primary hover:underline">
158
+ {inquiry.email}
159
+ </a>
160
+ </p>
161
+ </div>
162
+ <div className="space-y-1">
163
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
164
+ <Phone className="h-4 w-4" /> Phone
165
+ </span>
166
+ <p className="font-medium">{inquiry.phone || "Not provided"}</p>
167
+ </div>
168
+ <div className="space-y-1">
169
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2">
170
+ <Building className="h-4 w-4" /> Company
171
+ </span>
172
+ <p className="font-medium">{inquiry.company || "Not provided"}</p>
173
+ </div>
174
+ </CardContent>
175
+ </Card>
176
+
177
+ <Card>
178
+ <CardHeader>
179
+ <CardTitle>Project Requirements</CardTitle>
180
+ </CardHeader>
181
+ <CardContent className="space-y-6">
182
+ <div className="grid gap-4 sm:grid-cols-3 bg-muted/50 p-4 rounded-lg">
183
+ <div>
184
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
185
+ Type
186
+ </span>
187
+ <p className="font-medium capitalize">{inquiry.projectType || "General"}</p>
188
+ </div>
189
+ <div>
190
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
191
+ <DollarSign className="h-4 w-4" /> Budget
192
+ </span>
193
+ <p className="font-medium">{inquiry.budget || "Not specified"}</p>
194
+ </div>
195
+ <div>
196
+ <span className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
197
+ <Clock className="h-4 w-4" /> Timeline
198
+ </span>
199
+ <p className="font-medium">{inquiry.timeline || "Flexible"}</p>
200
+ </div>
201
+ </div>
202
+
203
+ <div className="space-y-2">
204
+ <h4 className="font-medium">Project Description</h4>
205
+ <div className="p-4 bg-muted/30 rounded-md whitespace-pre-wrap text-sm border">
206
+ {inquiry.description}
207
+ </div>
208
+ </div>
209
+ </CardContent>
210
+ </Card>
211
+ </div>
212
+
213
+ {/* Admin Controls */}
214
+ <div className="space-y-6">
215
+ <Card className="border-primary/20 bg-primary/5">
216
+ <CardHeader>
217
+ <CardTitle>Management</CardTitle>
218
+ <CardDescription>Update status and add internal notes.</CardDescription>
219
+ </CardHeader>
220
+ <CardContent className="space-y-6">
221
+ <div className="space-y-2">
222
+ <Label htmlFor="status">Current Status</Label>
223
+ <Select value={status} onValueChange={setStatus}>
224
+ <SelectTrigger id="status" className="bg-background">
225
+ <SelectValue placeholder="Select status" />
226
+ </SelectTrigger>
227
+ <SelectContent>
228
+ {STATUS_OPTIONS.map((opt) => (
229
+ <SelectItem key={opt.value} value={opt.value}>
230
+ {opt.label}
231
+ </SelectItem>
232
+ ))}
233
+ </SelectContent>
234
+ </Select>
235
+ </div>
236
+
237
+ <div className="space-y-2">
238
+ <Label htmlFor="notes">Internal Notes (Not visible to client)</Label>
239
+ <Textarea
240
+ id="notes"
241
+ placeholder="Add background info, meeting notes, etc..."
242
+ className="min-h-[150px] bg-background"
243
+ value={notes}
244
+ onChange={(e) => setNotes(e.target.value)}
245
+ />
246
+ </div>
247
+
248
+ <Button
249
+ className="w-full"
250
+ onClick={handleSave}
251
+ disabled={saving || (status === inquiry.status && notes === (inquiry.notes || ""))}
252
+ >
253
+ {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
254
+ Save Changes
255
+ </Button>
256
+ </CardContent>
257
+ </Card>
258
+
259
+ <div className="text-xs text-muted-foreground text-center">
260
+ <p>Inquiry ID: <span className="font-mono">{inquiry.id}</span></p>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ )
266
+ }
app/admin/inquiries/page.tsx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { useSession } from "next-auth/react"
5
+ import { formatDistanceToNow } from "date-fns"
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7
+ import { Input } from "@/components/ui/input"
8
+ import { Badge } from "@/components/ui/badge"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Loader2, Search, Filter } from "lucide-react"
11
+ import Link from "next/link"
12
+
13
+ interface Inquiry {
14
+ id: string
15
+ name: string
16
+ email: string
17
+ projectType: string
18
+ budget: string | null
19
+ status: string
20
+ createdAt: string
21
+ }
22
+
23
+ const statusColors: Record<string, string> = {
24
+ NEW: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
25
+ IN_PROGRESS: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
26
+ QUOTED: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
27
+ WON: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
28
+ LOST: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300",
29
+ }
30
+
31
+ export default function InquiriesPage() {
32
+ const { data: session } = useSession()
33
+ const [inquiries, setInquiries] = useState<Inquiry[]>([])
34
+ const [loading, setLoading] = useState(true)
35
+ const [searchTerm, setSearchTerm] = useState("")
36
+
37
+ useEffect(() => {
38
+ if (session) {
39
+ fetchInquiries()
40
+ }
41
+ }, [session])
42
+
43
+ const fetchInquiries = async () => {
44
+ try {
45
+ const res = await fetch("http://localhost:5000/api/admin/inquiries", {
46
+ headers: {
47
+ Authorization: `Bearer ${session?.user?.id}`, // using user id as mock token for now
48
+ },
49
+ })
50
+ if (res.ok) {
51
+ const data = await res.json()
52
+ setInquiries(data)
53
+ }
54
+ } catch (error) {
55
+ console.error("Failed to fetch inquiries:", error)
56
+ } finally {
57
+ setLoading(false)
58
+ }
59
+ }
60
+
61
+ const filteredInquiries = inquiries.filter(
62
+ (inq) =>
63
+ inq.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
64
+ inq.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
65
+ (inq.projectType && inq.projectType.toLowerCase().includes(searchTerm.toLowerCase()))
66
+ )
67
+
68
+ if (loading) {
69
+ return (
70
+ <div className="flex h-[50vh] items-center justify-center">
71
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
72
+ </div>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <div className="space-y-6">
78
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
79
+ <div>
80
+ <h1 className="text-3xl font-bold tracking-tight">Inquiry Management</h1>
81
+ <p className="text-muted-foreground">Manage and track client quote requests.</p>
82
+ </div>
83
+ </div>
84
+
85
+ <Card>
86
+ <CardHeader>
87
+ <div className="flex items-center space-x-2">
88
+ <Search className="h-4 w-4 text-muted-foreground" />
89
+ <Input
90
+ placeholder="Search by name, email, or project type..."
91
+ value={searchTerm}
92
+ onChange={(e) => setSearchTerm(e.target.value)}
93
+ className="max-w-sm"
94
+ />
95
+ </div>
96
+ </CardHeader>
97
+ <CardContent>
98
+ <div className="rounded-md border">
99
+ <div className="grid grid-cols-12 gap-4 border-b bg-muted/50 p-4 font-medium text-sm text-muted-foreground">
100
+ <div className="col-span-3">Client</div>
101
+ <div className="col-span-2 hidden sm:block">Project</div>
102
+ <div className="col-span-2 hidden md:block">Budget</div>
103
+ <div className="col-span-2">Date</div>
104
+ <div className="col-span-2">Status</div>
105
+ <div className="col-span-1 text-right">Actions</div>
106
+ </div>
107
+
108
+ {filteredInquiries.length === 0 ? (
109
+ <div className="p-8 text-center text-muted-foreground">
110
+ No inquiries found.
111
+ </div>
112
+ ) : (
113
+ <div className="divide-y">
114
+ {filteredInquiries.map((inquiry) => (
115
+ <div key={inquiry.id} className="grid grid-cols-12 items-center gap-4 p-4 text-sm hover:bg-muted/50 transition-colors">
116
+ <div className="col-span-3">
117
+ <div className="font-medium truncate">{inquiry.name}</div>
118
+ <div className="text-xs text-muted-foreground truncate">{inquiry.email}</div>
119
+ </div>
120
+ <div className="col-span-2 hidden sm:block truncate capitalize">
121
+ {inquiry.projectType || '—'}
122
+ </div>
123
+ <div className="col-span-2 hidden md:block text-muted-foreground">
124
+ {inquiry.budget || '—'}
125
+ </div>
126
+ <div className="col-span-2 text-muted-foreground">
127
+ {formatDistanceToNow(new Date(inquiry.createdAt), { addSuffix: true })}
128
+ </div>
129
+ <div className="col-span-2">
130
+ <Badge variant="secondary" className={`${statusColors[inquiry.status] || ''} hover:bg-opacity-80`}>
131
+ {inquiry.status.replace('_', ' ')}
132
+ </Badge>
133
+ </div>
134
+ <div className="col-span-1 text-right">
135
+ <Button variant="ghost" size="sm" asChild>
136
+ <Link href={`/admin/inquiries/${inquiry.id}`}>
137
+ View
138
+ </Link>
139
+ </Button>
140
+ </div>
141
+ </div>
142
+ ))}
143
+ </div>
144
+ )}
145
+ </div>
146
+ </CardContent>
147
+ </Card>
148
+ </div>
149
+ )
150
+ }
app/admin/login/page.tsx ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { useForm } from "react-hook-form"
5
+ import { zodResolver } from "@hookform/resolvers/zod"
6
+ import * as z from "zod"
7
+ import { signIn } from "next-auth/react"
8
+ import { useRouter } from "next/navigation"
9
+ import { Loader2 } from "lucide-react"
10
+ import Link from "next/link"
11
+
12
+ import { Button } from "@/components/ui/button"
13
+ import {
14
+ Form,
15
+ FormControl,
16
+ FormField,
17
+ FormItem,
18
+ FormLabel,
19
+ FormMessage,
20
+ } from "@/components/ui/form"
21
+ import { Input } from "@/components/ui/input"
22
+ import {
23
+ Card,
24
+ CardContent,
25
+ CardDescription,
26
+ CardHeader,
27
+ CardTitle,
28
+ } from "@/components/ui/card"
29
+
30
+ const formSchema = z.object({
31
+ email: z.string().email("Invalid email address"),
32
+ password: z.string().min(1, "Password is required"),
33
+ })
34
+
35
+ export default function LoginPage() {
36
+ const router = useRouter()
37
+ const [isLoading, setIsLoading] = useState(false)
38
+ const [error, setError] = useState<string | null>(null)
39
+
40
+ const form = useForm<z.infer<typeof formSchema>>({
41
+ resolver: zodResolver(formSchema),
42
+ defaultValues: {
43
+ email: "",
44
+ password: "",
45
+ },
46
+ })
47
+
48
+ async function onSubmit(values: z.infer<typeof formSchema>) {
49
+ setIsLoading(true)
50
+ setError(null)
51
+
52
+ try {
53
+ const result = await signIn("credentials", {
54
+ email: values.email,
55
+ password: values.password,
56
+ redirect: false,
57
+ })
58
+
59
+ if (result?.error) {
60
+ setError("Invalid email or password")
61
+ return
62
+ }
63
+
64
+ router.push("/admin/dashboard")
65
+ router.refresh()
66
+ } catch (error) {
67
+ setError("Something went wrong. Please try again.")
68
+ } finally {
69
+ setIsLoading(false)
70
+ }
71
+ }
72
+
73
+ return (
74
+ <div className="flex min-h-screen items-center justify-center bg-gray-100 p-4 dark:bg-gray-900">
75
+ <Card className="w-full max-w-md">
76
+ <CardHeader className="space-y-1">
77
+ <CardTitle className="text-2xl font-bold text-center">
78
+ Admin Login
79
+ </CardTitle>
80
+ <CardDescription className="text-center">
81
+ Enter your credentials to access the dashboard
82
+ </CardDescription>
83
+ </CardHeader>
84
+ <CardContent>
85
+ <Form {...form}>
86
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
87
+ <FormField
88
+ control={form.control}
89
+ name="email"
90
+ render={({ field }) => (
91
+ <FormItem>
92
+ <FormLabel>Email</FormLabel>
93
+ <FormControl>
94
+ <Input placeholder="admin@example.com" {...field} />
95
+ </FormControl>
96
+ <FormMessage />
97
+ </FormItem>
98
+ )}
99
+ />
100
+ <FormField
101
+ control={form.control}
102
+ name="password"
103
+ render={({ field }) => (
104
+ <FormItem>
105
+ <FormLabel>Password</FormLabel>
106
+ <FormControl>
107
+ <Input type="password" placeholder="••••••" {...field} />
108
+ </FormControl>
109
+ <FormMessage />
110
+ </FormItem>
111
+ )}
112
+ />
113
+ <div className="flex justify-end">
114
+ <Link
115
+ href="/admin/forgot-password"
116
+ className="text-sm font-medium text-primary hover:underline"
117
+ >
118
+ Forgot password?
119
+ </Link>
120
+ </div>
121
+ {error && (
122
+ <div className="text-sm text-red-500 font-medium text-center">
123
+ {error}
124
+ </div>
125
+ )}
126
+ <Button type="submit" className="w-full" disabled={isLoading}>
127
+ {isLoading ? (
128
+ <>
129
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
130
+ Signing in...
131
+ </>
132
+ ) : (
133
+ "Sign In"
134
+ )}
135
+ </Button>
136
+ </form>
137
+ </Form>
138
+ </CardContent>
139
+ </Card>
140
+ </div>
141
+ )
142
+ }
app/admin/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function AdminRoot() {
4
+ redirect("/admin/dashboard");
5
+ }
app/admin/portfolio/page.tsx ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect, useCallback } from "react"
4
+ import { useSession } from "next-auth/react"
5
+ import { useRouter } from "next/navigation"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
8
+ import { Input } from "@/components/ui/input"
9
+ import { z } from "zod"
10
+
11
+ const portfolioSchema = z.object({
12
+ title: z.string().min(1, "Title is required"),
13
+ slug: z.string().min(1, "Slug is required").regex(/^[a-z0-9-]+$/, "Slug must be lowercase letters, numbers, and hyphens only"),
14
+ description: z.string().min(1, "Description is required"),
15
+ technologies: z.string().optional(),
16
+ imageUrl: z.string().optional(),
17
+ clientName: z.string().optional(),
18
+ industry: z.string().optional(),
19
+ })
20
+ import {
21
+ Dialog,
22
+ DialogContent,
23
+ DialogDescription,
24
+ DialogFooter,
25
+ DialogHeader,
26
+ DialogTitle,
27
+ } from "@/components/ui/dialog"
28
+ import {
29
+ AlertDialog,
30
+ AlertDialogAction,
31
+ AlertDialogCancel,
32
+ AlertDialogContent,
33
+ AlertDialogDescription,
34
+ AlertDialogFooter,
35
+ AlertDialogHeader,
36
+ AlertDialogTitle,
37
+ } from "@/components/ui/alert-dialog"
38
+ import { Plus, Pencil, Trash2, ExternalLink, Loader2 } from "lucide-react"
39
+
40
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
41
+
42
+ type PortfolioItem = {
43
+ id: string
44
+ title: string
45
+ slug: string
46
+ description: string
47
+ technologies: string | null
48
+ imageUrl: string | null
49
+ clientName: string | null
50
+ industry: string | null
51
+ testimonials: string | null
52
+ createdAt: string
53
+ updatedAt: string
54
+ author?: { name: string | null; email: string }
55
+ }
56
+
57
+ type FormData = {
58
+ title: string
59
+ slug: string
60
+ description: string
61
+ technologies: string
62
+ imageUrl: string
63
+ clientName: string
64
+ industry: string
65
+ }
66
+
67
+ const emptyForm: FormData = {
68
+ title: "",
69
+ slug: "",
70
+ description: "",
71
+ technologies: "",
72
+ imageUrl: "",
73
+ clientName: "",
74
+ industry: "",
75
+ }
76
+
77
+ function slugify(str: string): string {
78
+ return str
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9]+/g, "-")
81
+ .replace(/^-|-$/g, "")
82
+ }
83
+
84
+ export default function AdminPortfolioPage() {
85
+ const { data: session, status } = useSession()
86
+ const router = useRouter()
87
+
88
+ const [items, setItems] = useState<PortfolioItem[]>([])
89
+ const [loading, setLoading] = useState(true)
90
+ const [saving, setSaving] = useState(false)
91
+ const [formError, setFormError] = useState("")
92
+
93
+ // Dialog state
94
+ const [dialogOpen, setDialogOpen] = useState(false)
95
+ const [editingId, setEditingId] = useState<string | null>(null)
96
+ const [form, setForm] = useState<FormData>(emptyForm)
97
+ const [imageFile, setImageFile] = useState<File | null>(null)
98
+
99
+ // Delete dialog state
100
+ const [deleteId, setDeleteId] = useState<string | null>(null)
101
+ const [deleteTitle, setDeleteTitle] = useState("")
102
+
103
+ // Redirect if not authenticated
104
+ useEffect(() => {
105
+ if (status === "unauthenticated") router.push("/admin/login")
106
+ }, [status, router])
107
+
108
+ // Fetch portfolio items
109
+ const fetchItems = useCallback(async () => {
110
+ try {
111
+ const res = await fetch(`${API_URL}/admin/portfolio`, {
112
+ headers: { Authorization: `Bearer ${(session as any)?.accessToken || "session"}` },
113
+ credentials: "include",
114
+ })
115
+ if (res.ok) {
116
+ const data = await res.json()
117
+ setItems(data)
118
+ }
119
+ } catch (err) {
120
+ console.error("Failed to fetch portfolio items:", err)
121
+ } finally {
122
+ setLoading(false)
123
+ }
124
+ }, [session])
125
+
126
+ useEffect(() => {
127
+ if (status === "authenticated") fetchItems()
128
+ }, [status, fetchItems])
129
+
130
+ // Auto-generate slug from title
131
+ const handleTitleChange = (value: string) => {
132
+ setForm((prev) => ({
133
+ ...prev,
134
+ title: value,
135
+ // Only auto-generate slug if creating new or slug hasn't been manually edited
136
+ slug: editingId ? prev.slug : slugify(value),
137
+ }))
138
+ }
139
+
140
+ // Open create dialog
141
+ const openCreate = () => {
142
+ setEditingId(null)
143
+ setForm(emptyForm)
144
+ setImageFile(null)
145
+ setFormError("")
146
+ setDialogOpen(true)
147
+ }
148
+
149
+ // Open edit dialog
150
+ const openEdit = (item: PortfolioItem) => {
151
+ setEditingId(item.id)
152
+ setForm({
153
+ title: item.title,
154
+ slug: item.slug,
155
+ description: item.description,
156
+ technologies: item.technologies || "",
157
+ imageUrl: item.imageUrl || "",
158
+ clientName: item.clientName || "",
159
+ industry: item.industry || "",
160
+ })
161
+ setImageFile(null)
162
+ setFormError("")
163
+ setDialogOpen(true)
164
+ }
165
+
166
+ // Submit create/edit
167
+ const handleSubmit = async () => {
168
+ try {
169
+ // Validate
170
+ portfolioSchema.parse(form)
171
+ } catch (error: any) {
172
+ if (error instanceof z.ZodError) {
173
+ return setFormError(error.errors[0].message)
174
+ }
175
+ return setFormError("Invalid form data")
176
+ }
177
+
178
+ setSaving(true)
179
+ setFormError("")
180
+
181
+ try {
182
+ let uploadedImageUrl = form.imageUrl
183
+
184
+ // Upload image if selected
185
+ if (imageFile) {
186
+ const formData = new FormData()
187
+ formData.append("file", imageFile)
188
+
189
+ const uploadRes = await fetch(`${API_URL}/upload`, {
190
+ method: "POST",
191
+ headers: {
192
+ Authorization: `Bearer ${(session as any)?.accessToken || "session"}`,
193
+ },
194
+ body: formData,
195
+ })
196
+
197
+ if (!uploadRes.ok) {
198
+ const uploadData = await uploadRes.json()
199
+ throw new Error(uploadData.error || "Failed to upload image")
200
+ }
201
+
202
+ const uploadData = await uploadRes.json()
203
+ uploadedImageUrl = uploadData.url
204
+ }
205
+
206
+ const url = editingId
207
+ ? `${API_URL}/admin/portfolio/${editingId}`
208
+ : `${API_URL}/admin/portfolio`
209
+
210
+ const res = await fetch(url, {
211
+ method: editingId ? "PATCH" : "POST",
212
+ headers: {
213
+ "Content-Type": "application/json",
214
+ Authorization: `Bearer ${(session as any)?.accessToken || "session"}`,
215
+ },
216
+ credentials: "include",
217
+ body: JSON.stringify({
218
+ title: form.title.trim(),
219
+ slug: form.slug.trim(),
220
+ description: form.description.trim(),
221
+ technologies: form.technologies.trim() || null,
222
+ imageUrl: uploadedImageUrl.trim() || null,
223
+ clientName: form.clientName.trim() || null,
224
+ industry: form.industry.trim() || null,
225
+ }),
226
+ })
227
+
228
+ const data = await res.json()
229
+
230
+ if (!res.ok) {
231
+ setFormError(data.error || "Something went wrong")
232
+ return
233
+ }
234
+
235
+ setDialogOpen(false)
236
+ fetchItems()
237
+ } catch (err: any) {
238
+ setFormError(err.message || "Network error. Please try again.")
239
+ } finally {
240
+ setSaving(false)
241
+ }
242
+ }
243
+
244
+ // Delete
245
+ const handleDelete = async () => {
246
+ if (!deleteId) return
247
+
248
+ try {
249
+ await fetch(`${API_URL}/admin/portfolio/${deleteId}`, {
250
+ method: "DELETE",
251
+ headers: { Authorization: `Bearer ${(session as any)?.accessToken || "session"}` },
252
+ credentials: "include",
253
+ })
254
+ setDeleteId(null)
255
+ fetchItems()
256
+ } catch (err) {
257
+ console.error("Delete failed:", err)
258
+ }
259
+ }
260
+
261
+ if (status === "loading" || loading) {
262
+ return (
263
+ <div className="flex items-center justify-center min-h-[60vh]">
264
+ <Loader2 className="h-8 w-8 animate-spin text-primary" suppressHydrationWarning />
265
+ </div>
266
+ )
267
+ }
268
+
269
+ return (
270
+ <div className="p-8 space-y-8">
271
+ {/* Header */}
272
+ <div className="flex items-center justify-between">
273
+ <div>
274
+ <h1 className="text-3xl font-bold">Portfolio</h1>
275
+ <p className="text-muted-foreground mt-1">Manage your portfolio projects</p>
276
+ </div>
277
+ <Button onClick={openCreate}>
278
+ <Plus className="mr-2 h-4 w-4" />
279
+ New Project
280
+ </Button>
281
+ </div>
282
+
283
+ {/* Portfolio items list */}
284
+ {items.length === 0 ? (
285
+ <Card>
286
+ <CardContent className="flex flex-col items-center justify-center py-16 text-center">
287
+ <p className="text-muted-foreground mb-4">No portfolio items yet.</p>
288
+ <Button onClick={openCreate} variant="outline">
289
+ <Plus className="mr-2 h-4 w-4" />
290
+ Create Your First Project
291
+ </Button>
292
+ </CardContent>
293
+ </Card>
294
+ ) : (
295
+ <div className="grid gap-4">
296
+ {items.map((item) => (
297
+ <Card key={item.id} className="hover:shadow-md transition-shadow">
298
+ <CardContent className="flex items-center justify-between py-4">
299
+ <div className="flex-1 min-w-0">
300
+ <div className="flex items-center gap-3">
301
+ <h3 className="font-semibold truncate">{item.title}</h3>
302
+ {item.industry && (
303
+ <span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full text-xs">
304
+ {item.industry}
305
+ </span>
306
+ )}
307
+ </div>
308
+ <p className="text-sm text-muted-foreground mt-1 line-clamp-1">
309
+ {item.description}
310
+ </p>
311
+ <div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
312
+ <span className="font-mono">/{item.slug}</span>
313
+ {item.technologies && (
314
+ <span>{item.technologies}</span>
315
+ )}
316
+ <span>{new Date(item.createdAt).toLocaleDateString()}</span>
317
+ </div>
318
+ </div>
319
+ <div className="flex items-center gap-2 ml-4">
320
+ <Button variant="ghost" size="icon-sm" asChild>
321
+ <a href={`/portfolio/${item.slug}`} target="_blank" rel="noopener noreferrer">
322
+ <ExternalLink className="h-4 w-4" />
323
+ </a>
324
+ </Button>
325
+ <Button variant="ghost" size="icon-sm" onClick={() => openEdit(item)}>
326
+ <Pencil className="h-4 w-4" />
327
+ </Button>
328
+ <Button
329
+ variant="ghost"
330
+ size="icon-sm"
331
+ className="text-destructive hover:text-destructive"
332
+ onClick={() => { setDeleteId(item.id); setDeleteTitle(item.title) }}
333
+ >
334
+ <Trash2 className="h-4 w-4" />
335
+ </Button>
336
+ </div>
337
+ </CardContent>
338
+ </Card>
339
+ ))}
340
+ </div>
341
+ )}
342
+
343
+ {/* Create/Edit Dialog */}
344
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
345
+ <DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
346
+ <DialogHeader>
347
+ <DialogTitle>{editingId ? "Edit Project" : "New Project"}</DialogTitle>
348
+ <DialogDescription>
349
+ {editingId ? "Update the project details below." : "Fill in the details for your new portfolio project."}
350
+ </DialogDescription>
351
+ </DialogHeader>
352
+ <div className="space-y-4 py-4">
353
+ <div>
354
+ <label className="text-sm font-medium mb-1 block">Title *</label>
355
+ <Input
356
+ value={form.title}
357
+ onChange={(e) => handleTitleChange(e.target.value)}
358
+ placeholder="My Awesome Project"
359
+ />
360
+ </div>
361
+ <div>
362
+ <label className="text-sm font-medium mb-1 block">Slug *</label>
363
+ <Input
364
+ value={form.slug}
365
+ onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))}
366
+ placeholder="my-awesome-project"
367
+ className="font-mono text-sm"
368
+ />
369
+ <p className="text-xs text-muted-foreground mt-1">
370
+ URL: /portfolio/{form.slug || "..."}
371
+ </p>
372
+ </div>
373
+ <div>
374
+ <label className="text-sm font-medium mb-1 block">Description *</label>
375
+ <textarea
376
+ className="flex min-h-[80px] w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
377
+ value={form.description}
378
+ onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))}
379
+ placeholder="A brief description of the project..."
380
+ />
381
+ </div>
382
+ <div>
383
+ <label className="text-sm font-medium mb-1 block">Technologies</label>
384
+ <Input
385
+ value={form.technologies}
386
+ onChange={(e) => setForm((p) => ({ ...p, technologies: e.target.value }))}
387
+ placeholder="Next.js, React, Tailwind CSS"
388
+ />
389
+ <p className="text-xs text-muted-foreground mt-1">Comma-separated list</p>
390
+ </div>
391
+ <div className="grid grid-cols-2 gap-4">
392
+ <div>
393
+ <label className="text-sm font-medium mb-1 block">Client Name</label>
394
+ <Input
395
+ value={form.clientName}
396
+ onChange={(e) => setForm((p) => ({ ...p, clientName: e.target.value }))}
397
+ placeholder="Acme Corp"
398
+ />
399
+ </div>
400
+ <div>
401
+ <label className="text-sm font-medium mb-1 block">Industry</label>
402
+ <Input
403
+ value={form.industry}
404
+ onChange={(e) => setForm((p) => ({ ...p, industry: e.target.value }))}
405
+ placeholder="E-commerce"
406
+ />
407
+ </div>
408
+ </div>
409
+ <div>
410
+ <label className="text-sm font-medium mb-1 block">Image</label>
411
+ <div className="flex gap-4 items-start">
412
+ <div className="flex-1 space-y-2">
413
+ <Input
414
+ type="file"
415
+ accept="image/*"
416
+ onChange={(e) => {
417
+ if (e.target.files && e.target.files[0]) {
418
+ setImageFile(e.target.files[0])
419
+ }
420
+ }}
421
+ />
422
+ <p className="text-xs text-muted-foreground">Or provide an image URL directly:</p>
423
+ <Input
424
+ value={form.imageUrl}
425
+ onChange={(e) => { setForm((p) => ({ ...p, imageUrl: e.target.value })); setImageFile(null) }}
426
+ placeholder="https://example.com/image.png"
427
+ />
428
+ </div>
429
+ {(imageFile || form.imageUrl) && (
430
+ <div className="w-24 h-24 rounded-md border shrink-0 overflow-hidden bg-muted">
431
+ <img
432
+ src={imageFile ? URL.createObjectURL(imageFile) : (form.imageUrl.startsWith("http") || form.imageUrl.startsWith("/")) ? form.imageUrl : `/${form.imageUrl}`}
433
+ alt="Preview"
434
+ className="w-full h-full object-cover"
435
+ />
436
+ </div>
437
+ )}
438
+ </div>
439
+ </div>
440
+ {formError && (
441
+ <p className="text-sm text-destructive font-medium">{formError}</p>
442
+ )}
443
+ </div>
444
+ <DialogFooter>
445
+ <Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
446
+ Cancel
447
+ </Button>
448
+ <Button onClick={handleSubmit} disabled={saving}>
449
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
450
+ {editingId ? "Save Changes" : "Create Project"}
451
+ </Button>
452
+ </DialogFooter>
453
+ </DialogContent>
454
+ </Dialog>
455
+
456
+ {/* Delete Confirmation Dialog */}
457
+ <AlertDialog open={!!deleteId} onOpenChange={(open) => { if (!open) setDeleteId(null) }}>
458
+ <AlertDialogContent>
459
+ <AlertDialogHeader>
460
+ <AlertDialogTitle>Delete &quot;{deleteTitle}&quot;?</AlertDialogTitle>
461
+ <AlertDialogDescription>
462
+ This action cannot be undone. This will permanently delete the portfolio item and remove it from the website.
463
+ </AlertDialogDescription>
464
+ </AlertDialogHeader>
465
+ <AlertDialogFooter>
466
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
467
+ <AlertDialogAction
468
+ onClick={handleDelete}
469
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
470
+ >
471
+ Delete
472
+ </AlertDialogAction>
473
+ </AlertDialogFooter>
474
+ </AlertDialogContent>
475
+ </AlertDialog>
476
+ </div>
477
+ )
478
+ }
app/admin/posts/page.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { RoleGuard } from "@/components/auth/RoleGuard"
4
+ import { Role } from "@/types/auth"
5
+
6
+ export default function PostsPage() {
7
+ return (
8
+ <RoleGuard allowedRoles={[Role.SUPER_ADMIN, Role.EDITOR]}>
9
+ <div className="p-8">
10
+ <h1 className="text-3xl font-bold mb-4">Content Management</h1>
11
+ <div className="border p-4 rounded bg-background">
12
+ <p>Super Admins and Editors can manage content here.</p>
13
+ {/* Blog/Portfolio management UI would go here */}
14
+ </div>
15
+ </div>
16
+ </RoleGuard>
17
+ )
18
+ }
app/admin/reset-password/page.tsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, Suspense } from "react"
4
+ import { useRouter, useSearchParams } from "next/navigation"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Label } from "@/components/ui/label"
8
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
9
+
10
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
11
+
12
+ function ResetPasswordForm() {
13
+ const router = useRouter()
14
+ const searchParams = useSearchParams()
15
+ const token = searchParams.get("token")
16
+
17
+ const [password, setPassword] = useState("")
18
+ const [confirmPassword, setConfirmPassword] = useState("")
19
+ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
20
+ const [errorMessage, setErrorMessage] = useState("")
21
+
22
+ const handleSubmit = async (e: React.FormEvent) => {
23
+ e.preventDefault()
24
+
25
+ if (password !== confirmPassword) {
26
+ setErrorMessage("Passwords do not match")
27
+ return
28
+ }
29
+
30
+ if (!token) {
31
+ setErrorMessage("Missing reset token")
32
+ return
33
+ }
34
+
35
+ setStatus("loading")
36
+ setErrorMessage("")
37
+
38
+ try {
39
+ const res = await fetch(`${API_URL}/auth/reset-password`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({ token, password }),
43
+ })
44
+
45
+ const data = await res.json()
46
+
47
+ if (res.ok) {
48
+ setStatus("success")
49
+ setTimeout(() => {
50
+ router.push("/admin/login")
51
+ }, 2000)
52
+ } else {
53
+ setStatus("error")
54
+ setErrorMessage(data.error || "Failed to reset password")
55
+ }
56
+ } catch (error) {
57
+ setStatus("error")
58
+ setErrorMessage("An error occurred")
59
+ }
60
+ }
61
+
62
+ if (!token) {
63
+ return (
64
+ <div className="text-destructive">
65
+ Invalid link. Missing token.
66
+ </div>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <form onSubmit={handleSubmit} className="space-y-4">
72
+ {status === "error" && (
73
+ <div className="bg-destructive/15 text-destructive p-3 rounded-md text-sm">
74
+ {errorMessage}
75
+ </div>
76
+ )}
77
+ {status === "success" && (
78
+ <div className="bg-green-100 text-green-700 p-3 rounded-md text-sm">
79
+ Password updated! Redirecting to login...
80
+ </div>
81
+ )}
82
+
83
+ <div className="space-y-2">
84
+ <Label htmlFor="password">New Password</Label>
85
+ <Input
86
+ id="password"
87
+ type="password"
88
+ required
89
+ value={password}
90
+ onChange={(e) => setPassword(e.target.value)}
91
+ disabled={status === "loading" || status === "success"}
92
+ />
93
+ </div>
94
+ <div className="space-y-2">
95
+ <Label htmlFor="confirmPassword">Confirm Password</Label>
96
+ <Input
97
+ id="confirmPassword"
98
+ type="password"
99
+ required
100
+ value={confirmPassword}
101
+ onChange={(e) => setConfirmPassword(e.target.value)}
102
+ disabled={status === "loading" || status === "success"}
103
+ />
104
+ </div>
105
+ <Button type="submit" className="w-full" disabled={status === "loading" || status === "success"}>
106
+ {status === "loading" ? "Resetting..." : "Reset Password"}
107
+ </Button>
108
+ </form>
109
+ )
110
+ }
111
+
112
+ export default function ResetPasswordPage() {
113
+ return (
114
+ <div className="flex min-h-screen items-center justify-center bg-muted/50 p-4">
115
+ <Card className="w-full max-w-md">
116
+ <CardHeader>
117
+ <CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
118
+ <CardDescription>
119
+ Enter your new password below.
120
+ </CardDescription>
121
+ </CardHeader>
122
+ <CardContent>
123
+ <Suspense fallback={<div>Loading...</div>}>
124
+ <ResetPasswordForm />
125
+ </Suspense>
126
+ </CardContent>
127
+ </Card>
128
+ </div>
129
+ )
130
+ }
app/admin/settings/page.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { RoleGuard } from "@/components/auth/RoleGuard"
4
+ import { Role } from "@/types/auth"
5
+
6
+ export default function SettingsPage() {
7
+ return (
8
+ <RoleGuard allowedRoles={[Role.SUPER_ADMIN]}>
9
+ <div className="p-8">
10
+ <h1 className="text-3xl font-bold mb-4">System Settings</h1>
11
+ <div className="border p-4 rounded bg-background">
12
+ <div className="bg-green-100 text-green-700 p-4 rounded mb-4">
13
+ <strong>Access Granted:</strong> You are viewing this page because you have the <strong>SUPER_ADMIN</strong> role.
14
+ </div>
15
+ <p>Settings configuration panel will be implemented here.</p>
16
+ </div>
17
+ </div>
18
+ </RoleGuard>
19
+ )
20
+ }
app/admin/testimonials/page.tsx ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState, useEffect, useCallback } from "react"
4
+ import { useSession } from "next-auth/react"
5
+ import { useRouter } from "next/navigation"
6
+ import { Button } from "@/components/ui/button"
7
+ import { Card, CardContent } from "@/components/ui/card"
8
+ import { Input } from "@/components/ui/input"
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from "@/components/ui/dialog"
17
+ import {
18
+ AlertDialog,
19
+ AlertDialogAction,
20
+ AlertDialogCancel,
21
+ AlertDialogContent,
22
+ AlertDialogDescription,
23
+ AlertDialogFooter,
24
+ AlertDialogHeader,
25
+ AlertDialogTitle,
26
+ } from "@/components/ui/alert-dialog"
27
+ import { Plus, Pencil, Trash2, Star, CheckCircle, XCircle, Loader2 } from "lucide-react"
28
+
29
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
30
+
31
+ type Testimonial = {
32
+ id: string
33
+ clientName: string
34
+ company: string | null
35
+ content: string
36
+ rating: number
37
+ imageUrl: string | null
38
+ approved: boolean
39
+ createdAt: string
40
+ }
41
+
42
+ type FormData = {
43
+ clientName: string
44
+ company: string
45
+ content: string
46
+ rating: number
47
+ imageUrl: string
48
+ }
49
+
50
+ const emptyForm: FormData = {
51
+ clientName: "",
52
+ company: "",
53
+ content: "",
54
+ rating: 5,
55
+ imageUrl: "",
56
+ }
57
+
58
+ export default function AdminTestimonialsPage() {
59
+ const { data: session, status } = useSession()
60
+ const router = useRouter()
61
+
62
+ const [items, setItems] = useState<Testimonial[]>([])
63
+ const [loading, setLoading] = useState(true)
64
+ const [saving, setSaving] = useState(false)
65
+ const [formError, setFormError] = useState("")
66
+
67
+ const [dialogOpen, setDialogOpen] = useState(false)
68
+ const [editingId, setEditingId] = useState<string | null>(null)
69
+ const [form, setForm] = useState<FormData>(emptyForm)
70
+
71
+ const [deleteId, setDeleteId] = useState<string | null>(null)
72
+ const [deleteName, setDeleteName] = useState("")
73
+
74
+ useEffect(() => {
75
+ if (status === "unauthenticated") router.push("/admin/login")
76
+ }, [status, router])
77
+
78
+ const fetchItems = useCallback(async () => {
79
+ try {
80
+ const res = await fetch(`${API_URL}/admin/testimonials`, {
81
+ headers: { Authorization: `Bearer ${(session as any)?.accessToken || "session"}` },
82
+ credentials: "include",
83
+ })
84
+ if (res.ok) {
85
+ const data = await res.json()
86
+ setItems(data)
87
+ }
88
+ } catch (err) {
89
+ console.error("Failed to fetch testimonials:", err)
90
+ } finally {
91
+ setLoading(false)
92
+ }
93
+ }, [session])
94
+
95
+ useEffect(() => {
96
+ if (status === "authenticated") fetchItems()
97
+ }, [status, fetchItems])
98
+
99
+ const openCreate = () => {
100
+ setEditingId(null)
101
+ setForm(emptyForm)
102
+ setFormError("")
103
+ setDialogOpen(true)
104
+ }
105
+
106
+ const openEdit = (item: Testimonial) => {
107
+ setEditingId(item.id)
108
+ setForm({
109
+ clientName: item.clientName,
110
+ company: item.company || "",
111
+ content: item.content,
112
+ rating: item.rating,
113
+ imageUrl: item.imageUrl || "",
114
+ })
115
+ setFormError("")
116
+ setDialogOpen(true)
117
+ }
118
+
119
+ const handleSubmit = async () => {
120
+ if (!form.clientName.trim()) return setFormError("Client name is required")
121
+ if (!form.content.trim()) return setFormError("Testimonial content is required")
122
+ if (form.rating < 1 || form.rating > 5) return setFormError("Rating must be between 1 and 5")
123
+
124
+ setSaving(true)
125
+ setFormError("")
126
+
127
+ try {
128
+ const url = editingId
129
+ ? `${API_URL}/admin/testimonials/${editingId}`
130
+ : `${API_URL}/admin/testimonials`
131
+
132
+ const res = await fetch(url, {
133
+ method: editingId ? "PATCH" : "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ Authorization: `Bearer ${(session as any)?.accessToken || "session"}`,
137
+ },
138
+ credentials: "include",
139
+ body: JSON.stringify({
140
+ clientName: form.clientName.trim(),
141
+ company: form.company.trim() || null,
142
+ content: form.content.trim(),
143
+ rating: form.rating,
144
+ imageUrl: form.imageUrl.trim() || null,
145
+ }),
146
+ })
147
+
148
+ const data = await res.json()
149
+ if (!res.ok) {
150
+ setFormError(data.error || "Something went wrong")
151
+ return
152
+ }
153
+
154
+ setDialogOpen(false)
155
+ fetchItems()
156
+ } catch (err) {
157
+ setFormError("Network error. Please try again.")
158
+ } finally {
159
+ setSaving(false)
160
+ }
161
+ }
162
+
163
+ const handleToggle = async (id: string) => {
164
+ try {
165
+ await fetch(`${API_URL}/admin/testimonials/${id}/toggle`, {
166
+ method: "PATCH",
167
+ headers: { Authorization: `Bearer ${(session as any)?.accessToken || "session"}` },
168
+ credentials: "include",
169
+ })
170
+ fetchItems()
171
+ } catch (err) {
172
+ console.error("Toggle failed:", err)
173
+ }
174
+ }
175
+
176
+ const handleDelete = async () => {
177
+ if (!deleteId) return
178
+ try {
179
+ await fetch(`${API_URL}/admin/testimonials/${deleteId}`, {
180
+ method: "DELETE",
181
+ headers: { Authorization: `Bearer ${(session as any)?.accessToken || "session"}` },
182
+ credentials: "include",
183
+ })
184
+ setDeleteId(null)
185
+ fetchItems()
186
+ } catch (err) {
187
+ console.error("Delete failed:", err)
188
+ }
189
+ }
190
+
191
+ if (status === "loading" || loading) {
192
+ return (
193
+ <div className="flex items-center justify-center min-h-[60vh]">
194
+ <Loader2 className="h-8 w-8 animate-spin text-primary" suppressHydrationWarning />
195
+ </div>
196
+ )
197
+ }
198
+
199
+ return (
200
+ <div className="p-8 space-y-8">
201
+ {/* Header */}
202
+ <div className="flex items-center justify-between">
203
+ <div>
204
+ <h1 className="text-3xl font-bold">Testimonials</h1>
205
+ <p className="text-muted-foreground mt-1">Manage client testimonials and approval status</p>
206
+ </div>
207
+ <Button onClick={openCreate}>
208
+ <Plus className="mr-2 h-4 w-4" suppressHydrationWarning />
209
+ New Testimonial
210
+ </Button>
211
+ </div>
212
+
213
+ {/* Testimonials list */}
214
+ {items.length === 0 ? (
215
+ <Card>
216
+ <CardContent className="flex flex-col items-center justify-center py-16 text-center">
217
+ <p className="text-muted-foreground mb-4">No testimonials yet.</p>
218
+ <Button onClick={openCreate} variant="outline">
219
+ <Plus className="mr-2 h-4 w-4" suppressHydrationWarning />
220
+ Add Your First Testimonial
221
+ </Button>
222
+ </CardContent>
223
+ </Card>
224
+ ) : (
225
+ <div className="grid gap-4">
226
+ {items.map((item) => (
227
+ <Card key={item.id} className="hover:shadow-md transition-shadow">
228
+ <CardContent className="flex items-start justify-between py-4 gap-4">
229
+ <div className="flex-1 min-w-0">
230
+ <div className="flex items-center gap-3 mb-1">
231
+ <h3 className="font-semibold">{item.clientName}</h3>
232
+ {item.company && (
233
+ <span className="text-sm text-muted-foreground">
234
+ {item.company}
235
+ </span>
236
+ )}
237
+ {/* Approval badge */}
238
+ {item.approved ? (
239
+ <span className="inline-flex items-center gap-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 px-2 py-0.5 rounded-full text-xs font-medium">
240
+ <CheckCircle className="h-3 w-3" suppressHydrationWarning />
241
+ Approved
242
+ </span>
243
+ ) : (
244
+ <span className="inline-flex items-center gap-1 bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 px-2 py-0.5 rounded-full text-xs font-medium">
245
+ <XCircle className="h-3 w-3" suppressHydrationWarning />
246
+ Pending
247
+ </span>
248
+ )}
249
+ </div>
250
+ <p className="text-sm text-muted-foreground line-clamp-2 mb-2">
251
+ &ldquo;{item.content}&rdquo;
252
+ </p>
253
+ <div className="flex items-center gap-1">
254
+ {[...Array(5)].map((_, j) => (
255
+ <Star
256
+ key={j}
257
+ className={`h-3.5 w-3.5 ${j < item.rating ? "fill-amber-400 text-amber-400" : "text-muted-foreground/30"}`}
258
+ suppressHydrationWarning
259
+ />
260
+ ))}
261
+ <span className="text-xs text-muted-foreground ml-2">
262
+ {new Date(item.createdAt).toLocaleDateString()}
263
+ </span>
264
+ </div>
265
+ </div>
266
+ <div className="flex items-center gap-1 shrink-0">
267
+ <Button
268
+ variant="ghost"
269
+ size="sm"
270
+ onClick={() => handleToggle(item.id)}
271
+ className={item.approved ? "text-amber-600 hover:text-amber-700" : "text-green-600 hover:text-green-700"}
272
+ >
273
+ {item.approved ? "Unpublish" : "Approve"}
274
+ </Button>
275
+ <Button variant="ghost" size="icon-sm" onClick={() => openEdit(item)}>
276
+ <Pencil className="h-4 w-4" suppressHydrationWarning />
277
+ </Button>
278
+ <Button
279
+ variant="ghost"
280
+ size="icon-sm"
281
+ className="text-destructive hover:text-destructive"
282
+ onClick={() => { setDeleteId(item.id); setDeleteName(item.clientName) }}
283
+ >
284
+ <Trash2 className="h-4 w-4" suppressHydrationWarning />
285
+ </Button>
286
+ </div>
287
+ </CardContent>
288
+ </Card>
289
+ ))}
290
+ </div>
291
+ )}
292
+
293
+ {/* Create/Edit Dialog */}
294
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
295
+ <DialogContent className="max-w-lg">
296
+ <DialogHeader>
297
+ <DialogTitle>{editingId ? "Edit Testimonial" : "New Testimonial"}</DialogTitle>
298
+ <DialogDescription>
299
+ {editingId ? "Update the testimonial details." : "Add a new client testimonial."}
300
+ </DialogDescription>
301
+ </DialogHeader>
302
+ <div className="space-y-4 py-4">
303
+ <div className="grid grid-cols-2 gap-4">
304
+ <div>
305
+ <label className="text-sm font-medium mb-1 block">Client Name *</label>
306
+ <Input
307
+ value={form.clientName}
308
+ onChange={(e) => setForm((p) => ({ ...p, clientName: e.target.value }))}
309
+ placeholder="Jane Doe"
310
+ />
311
+ </div>
312
+ <div>
313
+ <label className="text-sm font-medium mb-1 block">Company</label>
314
+ <Input
315
+ value={form.company}
316
+ onChange={(e) => setForm((p) => ({ ...p, company: e.target.value }))}
317
+ placeholder="Acme Inc."
318
+ />
319
+ </div>
320
+ </div>
321
+ <div>
322
+ <label className="text-sm font-medium mb-1 block">Testimonial *</label>
323
+ <textarea
324
+ className="flex min-h-[80px] w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
325
+ value={form.content}
326
+ onChange={(e) => setForm((p) => ({ ...p, content: e.target.value }))}
327
+ placeholder="What the client said about your work..."
328
+ />
329
+ </div>
330
+ <div>
331
+ <label className="text-sm font-medium mb-1 block">Rating (1-5)</label>
332
+ <div className="flex items-center gap-1">
333
+ {[1, 2, 3, 4, 5].map((val) => (
334
+ <button
335
+ key={val}
336
+ type="button"
337
+ onClick={() => setForm((p) => ({ ...p, rating: val }))}
338
+ className="p-1 hover:scale-110 transition-transform"
339
+ >
340
+ <Star
341
+ className={`h-6 w-6 ${val <= form.rating ? "fill-amber-400 text-amber-400" : "text-muted-foreground/30"}`}
342
+ suppressHydrationWarning
343
+ />
344
+ </button>
345
+ ))}
346
+ <span className="text-sm text-muted-foreground ml-2">{form.rating}/5</span>
347
+ </div>
348
+ </div>
349
+ <div>
350
+ <label className="text-sm font-medium mb-1 block">Avatar URL</label>
351
+ <Input
352
+ value={form.imageUrl}
353
+ onChange={(e) => setForm((p) => ({ ...p, imageUrl: e.target.value }))}
354
+ placeholder="https://example.com/avatar.jpg"
355
+ />
356
+ </div>
357
+ {formError && (
358
+ <p className="text-sm text-destructive font-medium">{formError}</p>
359
+ )}
360
+ </div>
361
+ <DialogFooter>
362
+ <Button variant="outline" onClick={() => setDialogOpen(false)} disabled={saving}>
363
+ Cancel
364
+ </Button>
365
+ <Button onClick={handleSubmit} disabled={saving}>
366
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" suppressHydrationWarning />}
367
+ {editingId ? "Save Changes" : "Add Testimonial"}
368
+ </Button>
369
+ </DialogFooter>
370
+ </DialogContent>
371
+ </Dialog>
372
+
373
+ {/* Delete Confirmation */}
374
+ <AlertDialog open={!!deleteId} onOpenChange={(open) => { if (!open) setDeleteId(null) }}>
375
+ <AlertDialogContent>
376
+ <AlertDialogHeader>
377
+ <AlertDialogTitle>Delete testimonial from &quot;{deleteName}&quot;?</AlertDialogTitle>
378
+ <AlertDialogDescription>
379
+ This action cannot be undone. This will permanently remove this testimonial.
380
+ </AlertDialogDescription>
381
+ </AlertDialogHeader>
382
+ <AlertDialogFooter>
383
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
384
+ <AlertDialogAction
385
+ onClick={handleDelete}
386
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
387
+ >
388
+ Delete
389
+ </AlertDialogAction>
390
+ </AlertDialogFooter>
391
+ </AlertDialogContent>
392
+ </AlertDialog>
393
+ </div>
394
+ )
395
+ }
app/api/auth/[...nextauth]/route.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import { handlers } from "@/lib/auth"
2
+ export const { GET, POST } = handlers
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,1494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @plugin "tailwindcss-animate";
4
+ @plugin "@tailwindcss/typography";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ /* ───────────────────────────────────────────────
9
+ NexaFlow Brand Palette
10
+ ─────────────────────────────────────────────── */
11
+
12
+ :root {
13
+ /* Rupam Portfolio Style - Neumorphic Light */
14
+ --background: 224 14% 90%;
15
+ --foreground: 215 25% 27%;
16
+ --card: 224 14% 90%;
17
+ --card-foreground: 215 25% 27%;
18
+ --popover: 224 14% 90%;
19
+ --popover-foreground: 215 25% 27%;
20
+
21
+ --primary: 248 73% 65%;
22
+ --primary-foreground: 0 0% 100%;
23
+
24
+ --secondary: 220 14% 96%;
25
+ --secondary-foreground: 215 25% 27%;
26
+
27
+ --muted: 220 14% 96%;
28
+ --muted-foreground: 215 16% 47%;
29
+
30
+ --accent: 248 73% 65%;
31
+ --accent-foreground: 0 0% 100%;
32
+
33
+ --destructive: 0 84% 60%;
34
+ --destructive-foreground: 0 0% 100%;
35
+
36
+ --border: 220 13% 91%;
37
+ --input: 220 13% 91%;
38
+ --ring: 248 73% 65%;
39
+ --radius: 0.625rem;
40
+
41
+ /* Semantic colors */
42
+ --success: 142 71% 45%;
43
+ --success-foreground: 0 0% 100%;
44
+ --warning: 38 92% 50%;
45
+ --warning-foreground: 0 0% 100%;
46
+ --info: 199 89% 48%;
47
+ --info-foreground: 0 0% 100%;
48
+
49
+ /* Chart colors */
50
+ --chart-1: 248 73% 65%;
51
+ --chart-2: 160 60% 45%;
52
+ --chart-3: 30 80% 55%;
53
+ --chart-4: 280 65% 60%;
54
+ --chart-5: 340 75% 55%;
55
+
56
+ /* Neumorphism shadows - Rupam Style */
57
+ --neu-shadow-light: #ffffff;
58
+ --neu-shadow-dark: #a3b1c6;
59
+ --neu-distance: 20px;
60
+ --neu-blur: 60px;
61
+ --bg-primary: hsl(224, 14%, 90%);
62
+ --bg-secondary: hsl(220, 14%, 96%);
63
+
64
+ /* Floating blobs */
65
+ --blob-1: hsl(248, 73%, 65%);
66
+ --blob-2: hsl(280, 65%, 60%);
67
+
68
+ /* Gradient colors for premium look */
69
+ --gradient-start: 248 73% 65%;
70
+ --gradient-mid: 270 70% 60%;
71
+ --gradient-end: 290 65% 55%;
72
+ }
73
+
74
+ .dark {
75
+ --background: 224 71% 4%;
76
+ --foreground: 210 20% 98%;
77
+ --card: 224 71% 4%;
78
+ --card-foreground: 210 20% 98%;
79
+ --popover: 224 71% 4%;
80
+ --popover-foreground: 210 20% 98%;
81
+
82
+ --primary: 248 73% 70%;
83
+ --primary-foreground: 0 0% 100%;
84
+
85
+ --secondary: 215 28% 17%;
86
+ --secondary-foreground: 210 20% 98%;
87
+
88
+ --muted: 215 28% 17%;
89
+ --muted-foreground: 218 11% 65%;
90
+
91
+ --accent: 248 73% 70%;
92
+ --accent-foreground: 0 0% 100%;
93
+
94
+ --destructive: 0 63% 31%;
95
+ --destructive-foreground: 210 20% 98%;
96
+
97
+ --border: 215 28% 17%;
98
+ --input: 215 28% 17%;
99
+ --ring: 248 73% 70%;
100
+
101
+ /* Semantic colors (dark) */
102
+ --success: 142 71% 35%;
103
+ --success-foreground: 0 0% 100%;
104
+ --warning: 38 92% 40%;
105
+ --warning-foreground: 0 0% 100%;
106
+ --info: 199 89% 38%;
107
+ --info-foreground: 0 0% 100%;
108
+
109
+ /* Chart colors (dark) */
110
+ --chart-1: 248 73% 70%;
111
+ --chart-2: 160 50% 40%;
112
+ --chart-3: 30 70% 50%;
113
+ --chart-4: 280 55% 55%;
114
+ --chart-5: 340 65% 50%;
115
+
116
+ /* Neumorphism shadows (dark) */
117
+ --neu-shadow-light: rgba(255, 255, 255, 0.05);
118
+ --neu-shadow-dark: rgba(0, 0, 0, 0.5);
119
+ --neu-distance: 20px;
120
+ --neu-blur: 60px;
121
+ --bg-primary: hsl(224, 71%, 4%);
122
+ --bg-secondary: hsl(215, 28%, 17%);
123
+
124
+ /* Floating blobs */
125
+ --blob-1: hsl(248, 73%, 70%);
126
+ --blob-2: hsl(280, 55%, 55%);
127
+ }
128
+
129
+ /* ───────────────────────────────────────────────
130
+ Theme Mapping (Tailwind v4)
131
+ ─────────────────────────────────────────────── */
132
+
133
+ @theme inline {
134
+ --color-background: hsl(var(--background));
135
+ --color-foreground: hsl(var(--foreground));
136
+ --color-card: hsl(var(--card));
137
+ --color-card-foreground: hsl(var(--card-foreground));
138
+ --color-popover: hsl(var(--popover));
139
+ --color-popover-foreground: hsl(var(--popover-foreground));
140
+ --color-primary: hsl(var(--primary));
141
+ --color-primary-foreground: hsl(var(--primary-foreground));
142
+ --color-secondary: hsl(var(--secondary));
143
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
144
+ --color-muted: hsl(var(--muted));
145
+ --color-muted-foreground: hsl(var(--muted-foreground));
146
+ --color-accent: hsl(var(--accent));
147
+ --color-accent-foreground: hsl(var(--accent-foreground));
148
+ --color-destructive: hsl(var(--destructive));
149
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
150
+ --color-border: hsl(var(--border));
151
+ --color-input: hsl(var(--input));
152
+ --color-ring: hsl(var(--ring));
153
+ --color-chart-1: hsl(var(--chart-1));
154
+ --color-chart-2: hsl(var(--chart-2));
155
+ --color-chart-3: hsl(var(--chart-3));
156
+ --color-chart-4: hsl(var(--chart-4));
157
+ --color-chart-5: hsl(var(--chart-5));
158
+
159
+ /* Semantic */
160
+ --color-success: hsl(var(--success));
161
+ --color-success-foreground: hsl(var(--success-foreground));
162
+ --color-warning: hsl(var(--warning));
163
+ --color-warning-foreground: hsl(var(--warning-foreground));
164
+ --color-info: hsl(var(--info));
165
+ --color-info-foreground: hsl(var(--info-foreground));
166
+
167
+ /* Radius */
168
+ --radius-sm: calc(var(--radius) - 4px);
169
+ --radius-md: calc(var(--radius) - 2px);
170
+ --radius-lg: var(--radius);
171
+ --radius-xl: calc(var(--radius) + 4px);
172
+ --radius-2xl: calc(var(--radius) + 8px);
173
+ --radius-3xl: calc(var(--radius) + 12px);
174
+ }
175
+
176
+ /* ───────────────────────────────────────────────
177
+ Utilities
178
+ ─────────────────────────────────────────────── */
179
+
180
+ @layer utilities {
181
+ .container {
182
+ margin-inline: auto;
183
+ padding-inline: 1.5rem;
184
+ }
185
+
186
+ @media (min-width: 640px) {
187
+ .container {
188
+ max-width: 640px;
189
+ }
190
+ }
191
+
192
+ @media (min-width: 768px) {
193
+ .container {
194
+ max-width: 768px;
195
+ padding-inline: 2rem;
196
+ }
197
+ }
198
+
199
+ @media (min-width: 1024px) {
200
+ .container {
201
+ max-width: 1024px;
202
+ }
203
+ }
204
+
205
+ @media (min-width: 1280px) {
206
+ .container {
207
+ max-width: 1280px;
208
+ }
209
+ }
210
+
211
+ /* Smooth scroll behavior */
212
+ .smooth-scroll {
213
+ scroll-behavior: smooth;
214
+ -webkit-overflow-scrolling: touch;
215
+ }
216
+
217
+ /* Hide scrollbar but keep functionality */
218
+ .scrollbar-hide {
219
+ -ms-overflow-style: none;
220
+ scrollbar-width: none;
221
+ }
222
+
223
+ .scrollbar-hide::-webkit-scrollbar {
224
+ display: none;
225
+ }
226
+
227
+ /* Better focus styles */
228
+ .focus-ring {
229
+ @apply outline-none ring-2 ring-primary/50 ring-offset-2;
230
+ }
231
+
232
+ /* Text balance for better typography */
233
+ .text-balance {
234
+ text-wrap: balance;
235
+ }
236
+
237
+ /* Glass effect */
238
+ .glass {
239
+ background: hsl(var(--background) / 0.8);
240
+ backdrop-filter: blur(12px);
241
+ -webkit-backdrop-filter: blur(12px);
242
+ }
243
+
244
+ /* Gradient border */
245
+ .gradient-border {
246
+ position: relative;
247
+ }
248
+
249
+ .gradient-border::before {
250
+ content: '';
251
+ position: absolute;
252
+ inset: 0;
253
+ border-radius: inherit;
254
+ padding: 1px;
255
+ background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
256
+ -webkit-mask:
257
+ linear-gradient(#fff 0 0) content-box,
258
+ linear-gradient(#fff 0 0);
259
+ -webkit-mask-composite: xor;
260
+ mask-composite: exclude;
261
+ pointer-events: none;
262
+ }
263
+
264
+ /* Section spacing */
265
+ .section-py {
266
+ @apply py-16 md:py-20 lg:py-24;
267
+ }
268
+
269
+ /* Hide on scroll utility */
270
+ .hide-on-scroll {
271
+ transform: translateY(0);
272
+ transition: transform 0.3s ease;
273
+ }
274
+
275
+ .hide-on-scroll.scrolled {
276
+ transform: translateY(-100%);
277
+ }
278
+ }
279
+
280
+ /* ───────────────────────────────────────────────
281
+ Base Layer — Typography Scale & Defaults
282
+ ─────────────────────────────────────────────── */
283
+
284
+ @layer base {
285
+ * {
286
+ @apply border-border outline-ring/50;
287
+ }
288
+
289
+ body {
290
+ @apply bg-background text-foreground;
291
+ font-feature-settings: "rlig" 1, "calt" 1;
292
+ font-optical-sizing: auto;
293
+ -webkit-font-smoothing: antialiased;
294
+ -moz-osx-font-smoothing: grayscale;
295
+ text-rendering: optimizeLegibility;
296
+ }
297
+
298
+ h1, h2, h3, h4, h5, h6 {
299
+ font-weight: 700;
300
+ letter-spacing: -0.02em;
301
+ line-height: 1.2;
302
+ font-family: var(--font-heading), var(--font-sans);
303
+ }
304
+
305
+ h1 {
306
+ font-size: clamp(2.5rem, 5vw, 4rem);
307
+ }
308
+
309
+ h2 {
310
+ font-size: clamp(2rem, 4vw, 3rem);
311
+ }
312
+
313
+ h3 {
314
+ font-size: clamp(1.5rem, 3vw, 2rem);
315
+ }
316
+
317
+ h4 {
318
+ font-size: clamp(1.25rem, 2.5vw, 1.5rem);
319
+ }
320
+
321
+ /* Font family utilities */
322
+ .font-heading {
323
+ font-family: var(--font-heading), var(--font-sans);
324
+ }
325
+
326
+ /* Utility typography classes */
327
+ .text-body {
328
+ @apply text-base leading-relaxed;
329
+ }
330
+
331
+ .text-caption {
332
+ @apply text-sm text-muted-foreground;
333
+ }
334
+
335
+ .text-overline {
336
+ @apply text-xs font-semibold uppercase tracking-widest text-muted-foreground;
337
+ }
338
+ }
339
+
340
+ /* ───────────────────────────────────────────────
341
+ CSS Animations (replacing Framer Motion)
342
+ ─────────────────────────────────────────────── */
343
+
344
+ @keyframes fadeIn {
345
+ from {
346
+ opacity: 0;
347
+ }
348
+
349
+ to {
350
+ opacity: 1;
351
+ }
352
+ }
353
+
354
+ @keyframes fadeInUp {
355
+ from {
356
+ opacity: 0;
357
+ transform: translateY(20px);
358
+ }
359
+
360
+ to {
361
+ opacity: 1;
362
+ transform: translateY(0);
363
+ }
364
+ }
365
+
366
+ @keyframes fadeInDown {
367
+ from {
368
+ opacity: 0;
369
+ transform: translateY(-12px);
370
+ }
371
+
372
+ to {
373
+ opacity: 1;
374
+ transform: translateY(0);
375
+ }
376
+ }
377
+
378
+ @keyframes scaleIn {
379
+ from {
380
+ opacity: 0;
381
+ transform: scale(0.95);
382
+ }
383
+
384
+ to {
385
+ opacity: 1;
386
+ transform: scale(1);
387
+ }
388
+ }
389
+
390
+ @keyframes slideInLeft {
391
+ from {
392
+ opacity: 0;
393
+ transform: translateX(-20px);
394
+ }
395
+
396
+ to {
397
+ opacity: 1;
398
+ transform: translateX(0);
399
+ }
400
+ }
401
+
402
+ @keyframes slideInRight {
403
+ from {
404
+ opacity: 0;
405
+ transform: translateX(20px);
406
+ }
407
+
408
+ to {
409
+ opacity: 1;
410
+ transform: translateX(0);
411
+ }
412
+ }
413
+
414
+ @keyframes shimmer {
415
+ 0% {
416
+ background-position: -1000px 0;
417
+ }
418
+ 100% {
419
+ background-position: 1000px 0;
420
+ }
421
+ }
422
+
423
+ @keyframes float {
424
+ 0%, 100% {
425
+ transform: translateY(0px);
426
+ }
427
+ 50% {
428
+ transform: translateY(-10px);
429
+ }
430
+ }
431
+
432
+ @keyframes pulse-glow {
433
+ 0%, 100% {
434
+ opacity: 1;
435
+ transform: scale(1);
436
+ }
437
+ 50% {
438
+ opacity: 0.8;
439
+ transform: scale(1.05);
440
+ }
441
+ }
442
+
443
+ @keyframes gradient-shift {
444
+ 0%, 100% {
445
+ background-position: 0% 50%;
446
+ }
447
+ 50% {
448
+ background-position: 100% 50%;
449
+ }
450
+ }
451
+
452
+ .animate-fade-in {
453
+ animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
454
+ }
455
+
456
+ .animate-fade-in-up {
457
+ animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
458
+ }
459
+
460
+ .animate-fade-in-down {
461
+ animation: fadeInDown 0.5s ease-out both;
462
+ }
463
+
464
+ .animate-scale-in {
465
+ animation: scaleIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
466
+ }
467
+
468
+ .animate-slide-in-left {
469
+ animation: slideInLeft 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
470
+ }
471
+
472
+ .animate-slide-in-right {
473
+ animation: slideInRight 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
474
+ }
475
+
476
+ .animate-shimmer {
477
+ background: linear-gradient(
478
+ 90deg,
479
+ transparent 0%,
480
+ rgba(255, 255, 255, 0.1) 50%,
481
+ transparent 100%
482
+ );
483
+ background-size: 1000px 100%;
484
+ animation: shimmer 3s infinite;
485
+ }
486
+
487
+ .animate-float {
488
+ animation: float 3s ease-in-out infinite;
489
+ }
490
+
491
+ .animate-pulse-glow {
492
+ animation: pulse-glow 2s ease-in-out infinite;
493
+ }
494
+
495
+ .animate-gradient {
496
+ background-size: 200% 200%;
497
+ animation: gradient-shift 8s ease infinite;
498
+ }
499
+
500
+ /* Stagger delays */
501
+ .animate-delay-1 {
502
+ animation-delay: 0.1s;
503
+ }
504
+
505
+ .animate-delay-2 {
506
+ animation-delay: 0.2s;
507
+ }
508
+
509
+ .animate-delay-3 {
510
+ animation-delay: 0.3s;
511
+ }
512
+
513
+ .animate-delay-4 {
514
+ animation-delay: 0.4s;
515
+ }
516
+
517
+ .animate-delay-5 {
518
+ animation-delay: 0.5s;
519
+ }
520
+
521
+ .animate-delay-6 {
522
+ animation-delay: 0.6s;
523
+ }
524
+
525
+ .animate-delay-7 {
526
+ animation-delay: 0.7s;
527
+ }
528
+
529
+ /* Hover effects */
530
+ .hover-lift {
531
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1);
532
+ }
533
+
534
+ .hover-lift:hover {
535
+ transform: translateY(-4px);
536
+ }
537
+
538
+ .hover-glow {
539
+ transition: box-shadow 0.3s ease;
540
+ }
541
+
542
+ .hover-glow:hover {
543
+ box-shadow: 0 0 20px rgba(var(--primary), 0.3);
544
+ }
545
+
546
+ /* Neumorphism styles */
547
+ .neu-flat {
548
+ background: hsl(var(--background));
549
+ box-shadow:
550
+ var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow-dark),
551
+ calc(var(--neu-distance) * -1) calc(var(--neu-distance) * -1) var(--neu-blur) var(--neu-shadow-light);
552
+ border: none;
553
+ transition: all 0.3s ease;
554
+ }
555
+
556
+ .neu-flat:hover {
557
+ box-shadow:
558
+ calc(var(--neu-distance) * 1.25) calc(var(--neu-distance) * 1.25) calc(var(--neu-blur) * 1.25) var(--neu-shadow-dark),
559
+ calc(var(--neu-distance) * -1.25) calc(var(--neu-distance) * -1.25) calc(var(--neu-blur) * 1.25) var(--neu-shadow-light);
560
+ }
561
+
562
+ .neu-pressed {
563
+ background: hsl(var(--background));
564
+ box-shadow:
565
+ inset var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow-dark),
566
+ inset calc(var(--neu-distance) * -1) calc(var(--neu-distance) * -1) var(--neu-blur) var(--neu-shadow-light);
567
+ border: none;
568
+ transition: all 0.3s ease;
569
+ }
570
+
571
+ .neu-convex {
572
+ background: linear-gradient(145deg, hsl(var(--background)), hsl(var(--muted)));
573
+ box-shadow:
574
+ var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow-dark),
575
+ calc(var(--neu-distance) * -1) calc(var(--neu-distance) * -1) var(--neu-blur) var(--neu-shadow-light);
576
+ border: none;
577
+ transition: all 0.3s ease;
578
+ }
579
+
580
+ .neu-convex:hover {
581
+ box-shadow:
582
+ inset 4px 4px 8px var(--neu-shadow-dark),
583
+ inset -4px -4px 8px var(--neu-shadow-light);
584
+ }
585
+
586
+ .neu-concave {
587
+ background: linear-gradient(145deg, hsl(var(--muted)), hsl(var(--background)));
588
+ box-shadow:
589
+ inset 5px 5px 10px var(--neu-shadow-dark),
590
+ inset -5px -5px 10px var(--neu-shadow-light);
591
+ border: none;
592
+ transition: all 0.3s ease;
593
+ }
594
+
595
+ .neu-button {
596
+ background: hsl(var(--background));
597
+ box-shadow:
598
+ 8px 8px 16px var(--neu-shadow-dark),
599
+ -8px -8px 16px var(--neu-shadow-light);
600
+ border: none;
601
+ transition: all 0.3s ease;
602
+ }
603
+
604
+ .neu-button:hover {
605
+ transform: translateY(-2px);
606
+ box-shadow:
607
+ 10px 10px 20px var(--neu-shadow-dark),
608
+ -10px -10px 20px var(--neu-shadow-light);
609
+ }
610
+
611
+ .neu-button:active {
612
+ transform: translateY(0);
613
+ box-shadow:
614
+ inset 4px 4px 8px var(--neu-shadow-dark),
615
+ inset -4px -4px 8px var(--neu-shadow-light);
616
+ }
617
+
618
+ .neu-card {
619
+ background: hsl(var(--background));
620
+ box-shadow:
621
+ 12px 12px 24px var(--neu-shadow-dark),
622
+ -12px -12px 24px var(--neu-shadow-light);
623
+ border: none;
624
+ border-radius: 30px;
625
+ transition: all 0.3s ease;
626
+ }
627
+
628
+ .neu-card:hover {
629
+ box-shadow:
630
+ 16px 16px 32px var(--neu-shadow-dark),
631
+ -16px -16px 32px var(--neu-shadow-light);
632
+ }
633
+
634
+ .neu-input {
635
+ background: hsl(var(--background));
636
+ box-shadow:
637
+ inset 4px 4px 8px var(--neu-shadow-dark),
638
+ inset -4px -4px 8px var(--neu-shadow-light);
639
+ border: none;
640
+ border-radius: 12px;
641
+ padding: 12px 16px;
642
+ transition: all 0.3s ease;
643
+ }
644
+
645
+ .neu-input:focus {
646
+ box-shadow:
647
+ inset 6px 6px 12px var(--neu-shadow-dark),
648
+ inset -6px -6px 12px var(--neu-shadow-light);
649
+ outline: none;
650
+ }
651
+
652
+ /* Advanced animation utilities */
653
+ .animate-on-scroll {
654
+ opacity: 0;
655
+ animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
656
+ }
657
+
658
+ .bounce-subtle {
659
+ animation: bounceSubtle 2s ease-in-out infinite;
660
+ }
661
+
662
+ @keyframes bounceSubtle {
663
+ 0%, 100% {
664
+ transform: translateY(0);
665
+ }
666
+ 50% {
667
+ transform: translateY(-5px);
668
+ }
669
+ }
670
+
671
+ .rotate-slow {
672
+ animation: rotateSlow 20s linear infinite;
673
+ }
674
+
675
+ @keyframes rotateSlow {
676
+ from {
677
+ transform: rotate(0deg);
678
+ }
679
+ to {
680
+ transform: rotate(360deg);
681
+ }
682
+ }
683
+
684
+ .perspective-card {
685
+ perspective: 1000px;
686
+ transform-style: preserve-3d;
687
+ transition: all 0.3s ease;
688
+ }
689
+
690
+ /* Premium gradient text */
691
+ .gradient-text {
692
+ background: linear-gradient(135deg, hsl(var(--gradient-start)), hsl(var(--gradient-mid)), hsl(var(--gradient-end)));
693
+ background-size: 200% 200%;
694
+ -webkit-background-clip: text;
695
+ background-clip: text;
696
+ -webkit-text-fill-color: transparent;
697
+ animation: gradientShift 8s ease infinite;
698
+ }
699
+
700
+ @keyframes gradientShift {
701
+ 0%, 100% {
702
+ background-position: 0% 50%;
703
+ }
704
+ 50% {
705
+ background-position: 100% 50%;
706
+ }
707
+ }
708
+
709
+ /* Floating background blobs */
710
+ .floating-blob {
711
+ position: absolute;
712
+ border-radius: 50%;
713
+ filter: blur(80px);
714
+ opacity: 0.3;
715
+ animation: float 20s ease-in-out infinite;
716
+ pointer-events: none;
717
+ z-index: 0;
718
+ }
719
+
720
+ .floating-blob-1 {
721
+ background: var(--blob-1);
722
+ width: 400px;
723
+ height: 400px;
724
+ top: -200px;
725
+ right: -100px;
726
+ animation-delay: 0s;
727
+ }
728
+
729
+ .floating-blob-2 {
730
+ background: var(--blob-2);
731
+ width: 350px;
732
+ height: 350px;
733
+ bottom: -150px;
734
+ left: -100px;
735
+ animation-delay: 5s;
736
+ }
737
+
738
+ @keyframes float {
739
+ 0%, 100% {
740
+ transform: translate(0, 0) scale(1);
741
+ }
742
+ 33% {
743
+ transform: translate(30px, -30px) scale(1.1);
744
+ }
745
+ 66% {
746
+ transform: translate(-20px, 20px) scale(0.9);
747
+ }
748
+ }
749
+
750
+ /* Shimmer effect for premium feel */
751
+ .shimmer {
752
+ position: relative;
753
+ overflow: hidden;
754
+ }
755
+
756
+ .shimmer::after {
757
+ content: '';
758
+ position: absolute;
759
+ top: 0;
760
+ left: -100%;
761
+ width: 100%;
762
+ height: 100%;
763
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
764
+ animation: shimmer 3s infinite;
765
+ }
766
+
767
+ @keyframes shimmer {
768
+ 0% {
769
+ left: -100%;
770
+ }
771
+ 100% {
772
+ left: 100%;
773
+ }
774
+ }
775
+
776
+ /* Glow effect on hover */
777
+ .glow-on-hover {
778
+ position: relative;
779
+ transition: all 0.3s ease;
780
+ }
781
+
782
+ .glow-on-hover::before {
783
+ content: '';
784
+ position: absolute;
785
+ inset: -2px;
786
+ border-radius: inherit;
787
+ background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent)));
788
+ opacity: 0;
789
+ transition: opacity 0.3s ease;
790
+ z-index: -1;
791
+ filter: blur(10px);
792
+ }
793
+
794
+ .glow-on-hover:hover::before {
795
+ opacity: 0.6;
796
+ }
797
+
798
+ /* 3D Card Tilt Effect */
799
+ .card-3d {
800
+ transform-style: preserve-3d;
801
+ transition: transform 0.3s ease;
802
+ }
803
+
804
+ .card-3d:hover {
805
+ transform: translateY(-8px);
806
+ }
807
+
808
+ /* Scroll reveal animations */
809
+ @keyframes reveal-up {
810
+ from {
811
+ opacity: 0;
812
+ transform: translateY(40px);
813
+ }
814
+ to {
815
+ opacity: 1;
816
+ transform: translateY(0);
817
+ }
818
+ }
819
+
820
+ @keyframes reveal-left {
821
+ from {
822
+ opacity: 0;
823
+ transform: translateX(-40px);
824
+ }
825
+ to {
826
+ opacity: 1;
827
+ transform: translateX(0);
828
+ }
829
+ }
830
+
831
+ @keyframes reveal-right {
832
+ from {
833
+ opacity: 0;
834
+ transform: translateX(40px);
835
+ }
836
+ to {
837
+ opacity: 1;
838
+ transform: translateX(0);
839
+ }
840
+ }
841
+
842
+ @keyframes scale-in {
843
+ from {
844
+ opacity: 0;
845
+ transform: scale(0.9);
846
+ }
847
+ to {
848
+ opacity: 1;
849
+ transform: scale(1);
850
+ }
851
+ }
852
+
853
+ .reveal-up {
854
+ animation: reveal-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
855
+ }
856
+
857
+ .reveal-left {
858
+ animation: reveal-left 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
859
+ }
860
+
861
+ .reveal-right {
862
+ animation: reveal-right 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
863
+ }
864
+
865
+ .scale-in {
866
+ animation: scale-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
867
+ }
868
+
869
+ /* Magnetic button effect */
870
+ .magnetic-button {
871
+ position: relative;
872
+ transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
873
+ }
874
+
875
+ /* Text gradient animation */
876
+ .text-shimmer {
877
+ background: linear-gradient(90deg,
878
+ hsl(var(--foreground)) 0%,
879
+ hsl(var(--primary)) 50%,
880
+ hsl(var(--foreground)) 100%
881
+ );
882
+ background-size: 200% auto;
883
+ -webkit-background-clip: text;
884
+ background-clip: text;
885
+ -webkit-text-fill-color: transparent;
886
+ animation: text-shimmer 3s linear infinite;
887
+ }
888
+
889
+ @keyframes text-shimmer {
890
+ to {
891
+ background-position: 200% center;
892
+ }
893
+ }
894
+
895
+ /* Pulse animation for icons */
896
+ .pulse-icon {
897
+ animation: pulse-icon 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
898
+ }
899
+
900
+ @keyframes pulse-icon {
901
+ 0%, 100% {
902
+ opacity: 1;
903
+ transform: scale(1);
904
+ }
905
+ 50% {
906
+ opacity: 0.8;
907
+ transform: scale(1.05);
908
+ }
909
+ }
910
+
911
+ /* Stagger children animation */
912
+ .stagger-children > * {
913
+ opacity: 0;
914
+ animation: reveal-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
915
+ }
916
+
917
+ .stagger-children > *:nth-child(1) { animation-delay: 0.1s; }
918
+ .stagger-children > *:nth-child(2) { animation-delay: 0.2s; }
919
+ .stagger-children > *:nth-child(3) { animation-delay: 0.3s; }
920
+ .stagger-children > *:nth-child(4) { animation-delay: 0.4s; }
921
+ .stagger-children > *:nth-child(5) { animation-delay: 0.5s; }
922
+ .stagger-children > *:nth-child(6) { animation-delay: 0.6s; }
923
+
924
+ /* ───────────────────────────────────────────────
925
+ Creative UI Enhancements
926
+ ─────────────────────────────────────────────── */
927
+
928
+ /* Animated gradient mesh background */
929
+ .gradient-mesh {
930
+ background:
931
+ radial-gradient(at 40% 20%, hsla(248, 73%, 65%, 0.3) 0px, transparent 50%),
932
+ radial-gradient(at 80% 0%, hsla(280, 65%, 60%, 0.2) 0px, transparent 50%),
933
+ radial-gradient(at 0% 50%, hsla(199, 89%, 48%, 0.2) 0px, transparent 50%),
934
+ radial-gradient(at 80% 50%, hsla(340, 75%, 55%, 0.15) 0px, transparent 50%),
935
+ radial-gradient(at 0% 100%, hsla(30, 80%, 55%, 0.2) 0px, transparent 50%),
936
+ radial-gradient(at 80% 100%, hsla(160, 60%, 45%, 0.15) 0px, transparent 50%);
937
+ animation: gradient-mesh-move 20s ease infinite;
938
+ }
939
+
940
+ @keyframes gradient-mesh-move {
941
+ 0%, 100% {
942
+ background-position: 0% 0%, 100% 0%, 0% 50%, 100% 50%, 0% 100%, 100% 100%;
943
+ }
944
+ 25% {
945
+ background-position: 100% 0%, 0% 50%, 50% 0%, 0% 100%, 50% 50%, 0% 0%;
946
+ }
947
+ 50% {
948
+ background-position: 100% 100%, 0% 0%, 100% 50%, 50% 0%, 100% 0%, 50% 50%;
949
+ }
950
+ 75% {
951
+ background-position: 0% 100%, 100% 50%, 0% 0%, 100% 0%, 50% 50%, 100% 100%;
952
+ }
953
+ }
954
+
955
+ /* Noise texture overlay for depth */
956
+ .noise-overlay {
957
+ position: relative;
958
+ }
959
+
960
+ .noise-overlay::before {
961
+ content: '';
962
+ position: absolute;
963
+ inset: 0;
964
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
965
+ opacity: 0.03;
966
+ pointer-events: none;
967
+ z-index: 1;
968
+ }
969
+
970
+ /* Morphing blob shapes */
971
+ @keyframes morph-blob {
972
+ 0%, 100% {
973
+ border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
974
+ }
975
+ 25% {
976
+ border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%;
977
+ }
978
+ 50% {
979
+ border-radius: 50% 60% 30% 60% / 30% 60% 70% 40%;
980
+ }
981
+ 75% {
982
+ border-radius: 60% 40% 60% 30% / 70% 30% 50% 60%;
983
+ }
984
+ }
985
+
986
+ .morphing-blob {
987
+ animation: morph-blob 15s ease-in-out infinite;
988
+ }
989
+
990
+ /* Floating particles effect */
991
+ @keyframes float-particle {
992
+ 0%, 100% {
993
+ transform: translateY(0) translateX(0) rotate(0deg);
994
+ opacity: 0.3;
995
+ }
996
+ 25% {
997
+ transform: translateY(-100px) translateX(50px) rotate(90deg);
998
+ opacity: 0.6;
999
+ }
1000
+ 50% {
1001
+ transform: translateY(-200px) translateX(-30px) rotate(180deg);
1002
+ opacity: 0.3;
1003
+ }
1004
+ 75% {
1005
+ transform: translateY(-100px) translateX(-50px) rotate(270deg);
1006
+ opacity: 0.6;
1007
+ }
1008
+ }
1009
+
1010
+ .particle {
1011
+ position: absolute;
1012
+ width: 6px;
1013
+ height: 6px;
1014
+ border-radius: 50%;
1015
+ background: hsl(var(--primary));
1016
+ opacity: 0.3;
1017
+ animation: float-particle 20s ease-in-out infinite;
1018
+ }
1019
+
1020
+ /* Enhanced glass effect with animated border */
1021
+ .glass-card {
1022
+ background: hsl(var(--background) / 0.6);
1023
+ backdrop-filter: blur(20px);
1024
+ -webkit-backdrop-filter: blur(20px);
1025
+ border: 1px solid hsl(var(--border) / 0.3);
1026
+ position: relative;
1027
+ overflow: hidden;
1028
+ }
1029
+
1030
+ .glass-card::before {
1031
+ content: '';
1032
+ position: absolute;
1033
+ inset: 0;
1034
+ background: linear-gradient(
1035
+ 135deg,
1036
+ hsl(var(--primary) / 0.1) 0%,
1037
+ transparent 50%,
1038
+ hsl(var(--accent) / 0.1) 100%
1039
+ );
1040
+ opacity: 0;
1041
+ transition: opacity 0.5s ease;
1042
+ }
1043
+
1044
+ .glass-card:hover::before {
1045
+ opacity: 1;
1046
+ }
1047
+
1048
+ /* Animated gradient border */
1049
+ .animated-border {
1050
+ position: relative;
1051
+ background: hsl(var(--background));
1052
+ border-radius: var(--radius-lg);
1053
+ }
1054
+
1055
+ .animated-border::before {
1056
+ content: '';
1057
+ position: absolute;
1058
+ inset: -2px;
1059
+ border-radius: inherit;
1060
+ background: conic-gradient(
1061
+ from var(--angle, 0deg),
1062
+ hsl(var(--primary)),
1063
+ hsl(280, 65%, 60%),
1064
+ hsl(320, 75%, 55%),
1065
+ hsl(var(--primary))
1066
+ );
1067
+ z-index: -1;
1068
+ animation: rotate-gradient 4s linear infinite;
1069
+ }
1070
+
1071
+ @keyframes rotate-gradient {
1072
+ to {
1073
+ --angle: 360deg;
1074
+ }
1075
+ }
1076
+
1077
+ @property --angle {
1078
+ syntax: '<angle>';
1079
+ initial-value: 0deg;
1080
+ inherits: false;
1081
+ }
1082
+
1083
+ /* Split color text effect */
1084
+ .split-color-text {
1085
+ background: linear-gradient(
1086
+ 135deg,
1087
+ hsl(var(--foreground)) 0%,
1088
+ hsl(var(--foreground)) 50%,
1089
+ hsl(var(--primary)) 50%,
1090
+ hsl(var(--primary)) 100%
1091
+ );
1092
+ background-size: 200% 100%;
1093
+ background-position: 100% 0;
1094
+ -webkit-background-clip: text;
1095
+ background-clip: text;
1096
+ -webkit-text-fill-color: transparent;
1097
+ transition: background-position 0.5s ease;
1098
+ }
1099
+
1100
+ .split-color-text:hover {
1101
+ background-position: 0 0;
1102
+ }
1103
+
1104
+ /* 3D perspective card with depth */
1105
+ .card-3d-depth {
1106
+ transform-style: preserve-3d;
1107
+ perspective: 1000px;
1108
+ transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
1109
+ }
1110
+
1111
+ .card-3d-depth:hover {
1112
+ transform: rotateY(-5deg) rotateX(5deg) translateZ(20px);
1113
+ }
1114
+
1115
+ .card-3d-depth::before {
1116
+ content: '';
1117
+ position: absolute;
1118
+ inset: 0;
1119
+ background: linear-gradient(
1120
+ 135deg,
1121
+ hsl(var(--primary) / 0.1) 0%,
1122
+ transparent 100%
1123
+ );
1124
+ opacity: 0;
1125
+ transition: opacity 0.3s ease;
1126
+ border-radius: inherit;
1127
+ z-index: -1;
1128
+ }
1129
+
1130
+ .card-3d-depth:hover::before {
1131
+ opacity: 1;
1132
+ }
1133
+
1134
+ /* Spotlight hover effect */
1135
+ .spotlight-card {
1136
+ position: relative;
1137
+ overflow: hidden;
1138
+ }
1139
+
1140
+ .spotlight-card::after {
1141
+ content: '';
1142
+ position: absolute;
1143
+ width: 200px;
1144
+ height: 200px;
1145
+ background: radial-gradient(
1146
+ circle,
1147
+ hsl(var(--primary) / 0.15) 0%,
1148
+ transparent 70%
1149
+ );
1150
+ border-radius: 50%;
1151
+ pointer-events: none;
1152
+ opacity: 0;
1153
+ transform: translate(-50%, -50%);
1154
+ transition: opacity 0.3s ease;
1155
+ }
1156
+
1157
+ .spotlight-card:hover::after {
1158
+ opacity: 1;
1159
+ }
1160
+
1161
+ /* Typewriter effect */
1162
+ .typewriter {
1163
+ overflow: hidden;
1164
+ border-right: 2px solid hsl(var(--primary));
1165
+ white-space: nowrap;
1166
+ animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
1167
+ }
1168
+
1169
+ @keyframes typing {
1170
+ from { width: 0; }
1171
+ to { width: 100%; }
1172
+ }
1173
+
1174
+ @keyframes blink-caret {
1175
+ from, to { border-color: transparent; }
1176
+ 50% { border-color: hsl(var(--primary)); }
1177
+ }
1178
+
1179
+ /* Glitch text effect */
1180
+ .glitch-text {
1181
+ position: relative;
1182
+ }
1183
+
1184
+ .glitch-text::before,
1185
+ .glitch-text::after {
1186
+ content: attr(data-text);
1187
+ position: absolute;
1188
+ top: 0;
1189
+ left: 0;
1190
+ width: 100%;
1191
+ height: 100%;
1192
+ }
1193
+
1194
+ .glitch-text::before {
1195
+ animation: glitch-1 0.3s infinite linear alternate-reverse;
1196
+ color: hsl(0, 100%, 50%);
1197
+ z-index: -1;
1198
+ }
1199
+
1200
+ .glitch-text::after {
1201
+ animation: glitch-2 0.3s infinite linear alternate-reverse;
1202
+ color: hsl(180, 100%, 50%);
1203
+ z-index: -2;
1204
+ }
1205
+
1206
+ @keyframes glitch-1 {
1207
+ 0% { clip-path: inset(20% 0 60% 0); transform: translate(-2px, 2px); }
1208
+ 20% { clip-path: inset(60% 0 20% 0); transform: translate(2px, -2px); }
1209
+ 40% { clip-path: inset(40% 0 40% 0); transform: translate(-2px, 0); }
1210
+ 60% { clip-path: inset(80% 0 0% 0); transform: translate(0, 2px); }
1211
+ 80% { clip-path: inset(0% 0 80% 0); transform: translate(2px, 0); }
1212
+ 100% { clip-path: inset(50% 0 30% 0); transform: translate(-2px, -2px); }
1213
+ }
1214
+
1215
+ @keyframes glitch-2 {
1216
+ 0% { clip-path: inset(60% 0 20% 0); transform: translate(2px, -2px); }
1217
+ 20% { clip-path: inset(20% 0 60% 0); transform: translate(-2px, 2px); }
1218
+ 40% { clip-path: inset(80% 0 0% 0); transform: translate(2px, 0); }
1219
+ 60% { clip-path: inset(40% 0 40% 0); transform: translate(0, -2px); }
1220
+ 80% { clip-path: inset(50% 0 30% 0); transform: translate(-2px, 0); }
1221
+ 100% { clip-path: inset(0% 0 80% 0); transform: translate(2px, 2px); }
1222
+ }
1223
+
1224
+ /* Neon glow effect */
1225
+ .neon-glow {
1226
+ text-shadow:
1227
+ 0 0 5px hsl(var(--primary)),
1228
+ 0 0 10px hsl(var(--primary)),
1229
+ 0 0 20px hsl(var(--primary)),
1230
+ 0 0 40px hsl(var(--primary));
1231
+ animation: neon-pulse 2s ease-in-out infinite;
1232
+ }
1233
+
1234
+ @keyframes neon-pulse {
1235
+ 0%, 100% {
1236
+ text-shadow:
1237
+ 0 0 5px hsl(var(--primary)),
1238
+ 0 0 10px hsl(var(--primary)),
1239
+ 0 0 20px hsl(var(--primary)),
1240
+ 0 0 40px hsl(var(--primary));
1241
+ }
1242
+ 50% {
1243
+ text-shadow:
1244
+ 0 0 2px hsl(var(--primary)),
1245
+ 0 0 5px hsl(var(--primary)),
1246
+ 0 0 10px hsl(var(--primary)),
1247
+ 0 0 20px hsl(var(--primary));
1248
+ }
1249
+ }
1250
+
1251
+ /* Ripple button effect */
1252
+ .ripple-button {
1253
+ position: relative;
1254
+ overflow: hidden;
1255
+ }
1256
+
1257
+ .ripple-button::after {
1258
+ content: '';
1259
+ position: absolute;
1260
+ width: 100%;
1261
+ height: 100%;
1262
+ top: 0;
1263
+ left: 0;
1264
+ background: radial-gradient(circle, hsl(var(--primary-foreground) / 0.3) 10%, transparent 10%);
1265
+ background-size: 0 0;
1266
+ background-position: center;
1267
+ transition: background-size 0.5s ease;
1268
+ }
1269
+
1270
+ .ripple-button:active::after {
1271
+ background-size: 200% 200%;
1272
+ }
1273
+
1274
+ /* Aurora background effect */
1275
+ .aurora-bg {
1276
+ position: relative;
1277
+ overflow: hidden;
1278
+ }
1279
+
1280
+ .aurora-bg::before {
1281
+ content: '';
1282
+ position: absolute;
1283
+ top: -50%;
1284
+ left: -50%;
1285
+ width: 200%;
1286
+ height: 200%;
1287
+ background:
1288
+ conic-gradient(from 0deg at 50% 50%, hsla(248, 73%, 65%, 0.3) 0deg, transparent 60deg),
1289
+ conic-gradient(from 120deg at 50% 50%, hsla(280, 65%, 60%, 0.2) 0deg, transparent 60deg),
1290
+ conic-gradient(from 240deg at 50% 50%, hsla(199, 89%, 48%, 0.2) 0deg, transparent 60deg);
1291
+ animation: aurora-rotate 30s linear infinite;
1292
+ opacity: 0.5;
1293
+ }
1294
+
1295
+ @keyframes aurora-rotate {
1296
+ to {
1297
+ transform: rotate(360deg);
1298
+ }
1299
+ }
1300
+
1301
+ /* Scroll indicator */
1302
+ .scroll-indicator {
1303
+ width: 30px;
1304
+ height: 50px;
1305
+ border: 2px solid hsl(var(--primary) / 0.5);
1306
+ border-radius: 25px;
1307
+ position: relative;
1308
+ }
1309
+
1310
+ .scroll-indicator::before {
1311
+ content: '';
1312
+ position: absolute;
1313
+ top: 8px;
1314
+ left: 50%;
1315
+ width: 6px;
1316
+ height: 6px;
1317
+ background: hsl(var(--primary));
1318
+ border-radius: 50%;
1319
+ transform: translateX(-50%);
1320
+ animation: scroll-bounce 2s ease-in-out infinite;
1321
+ }
1322
+
1323
+ @keyframes scroll-bounce {
1324
+ 0%, 100% { top: 8px; opacity: 1; }
1325
+ 50% { top: 32px; opacity: 0.3; }
1326
+ }
1327
+
1328
+ /* Reveal animation on scroll */
1329
+ .reveal {
1330
+ opacity: 0;
1331
+ transform: translateY(30px);
1332
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1333
+ }
1334
+
1335
+ .reveal.visible {
1336
+ opacity: 1;
1337
+ transform: translateY(0);
1338
+ }
1339
+
1340
+ .reveal-left {
1341
+ opacity: 0;
1342
+ transform: translateX(-50px);
1343
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1344
+ }
1345
+
1346
+ .reveal-left.visible {
1347
+ opacity: 1;
1348
+ transform: translateX(0);
1349
+ }
1350
+
1351
+ .reveal-right {
1352
+ opacity: 0;
1353
+ transform: translateX(50px);
1354
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1355
+ }
1356
+
1357
+ .reveal-right.visible {
1358
+ opacity: 1;
1359
+ transform: translateX(0);
1360
+ }
1361
+
1362
+ .reveal-scale {
1363
+ opacity: 0;
1364
+ transform: scale(0.9);
1365
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1366
+ }
1367
+
1368
+ .reveal-scale.visible {
1369
+ opacity: 1;
1370
+ transform: scale(1);
1371
+ }
1372
+
1373
+ /* Interactive underline */
1374
+ .interactive-underline {
1375
+ position: relative;
1376
+ display: inline-block;
1377
+ }
1378
+
1379
+ .interactive-underline::after {
1380
+ content: '';
1381
+ position: absolute;
1382
+ bottom: -4px;
1383
+ left: 0;
1384
+ width: 0;
1385
+ height: 3px;
1386
+ background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--accent)));
1387
+ border-radius: 2px;
1388
+ transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1389
+ }
1390
+
1391
+ .interactive-underline:hover::after {
1392
+ width: 100%;
1393
+ }
1394
+
1395
+ /* Counter animation */
1396
+ .counter-animate {
1397
+ display: inline-block;
1398
+ animation: counter-pop 0.6s cubic-bezier(0.16, 1, 0.3, 1);
1399
+ }
1400
+
1401
+ @keyframes counter-pop {
1402
+ 0% { transform: scale(1); }
1403
+ 50% { transform: scale(1.2); }
1404
+ 100% { transform: scale(1); }
1405
+ }
1406
+
1407
+ /* Shine effect */
1408
+ .shine-effect {
1409
+ position: relative;
1410
+ overflow: hidden;
1411
+ }
1412
+
1413
+ .shine-effect::after {
1414
+ content: '';
1415
+ position: absolute;
1416
+ top: 0;
1417
+ left: -100%;
1418
+ width: 50%;
1419
+ height: 100%;
1420
+ background: linear-gradient(
1421
+ 90deg,
1422
+ transparent,
1423
+ hsl(var(--primary-foreground) / 0.2),
1424
+ transparent
1425
+ );
1426
+ transform: skewX(-25deg);
1427
+ transition: left 0.5s ease;
1428
+ }
1429
+
1430
+ .shine-effect:hover::after {
1431
+ left: 150%;
1432
+ }
1433
+
1434
+ /* Orbit animation */
1435
+ .orbit {
1436
+ animation: orbit-spin 20s linear infinite;
1437
+ }
1438
+
1439
+ @keyframes orbit-spin {
1440
+ from { transform: rotate(0deg) translateX(100px) rotate(0deg); }
1441
+ to { transform: rotate(360deg) translateX(100px) rotate(-360deg); }
1442
+ }
1443
+
1444
+ /* Wave text effect */
1445
+ .wave-text span {
1446
+ display: inline-block;
1447
+ animation: wave 1s ease-in-out infinite;
1448
+ }
1449
+
1450
+ .wave-text span:nth-child(1) { animation-delay: 0s; }
1451
+ .wave-text span:nth-child(2) { animation-delay: 0.1s; }
1452
+ .wave-text span:nth-child(3) { animation-delay: 0.2s; }
1453
+ .wave-text span:nth-child(4) { animation-delay: 0.3s; }
1454
+ .wave-text span:nth-child(5) { animation-delay: 0.4s; }
1455
+ .wave-text span:nth-child(6) { animation-delay: 0.5s; }
1456
+
1457
+ @keyframes wave {
1458
+ 0%, 100% { transform: translateY(0); }
1459
+ 50% { transform: translateY(-10px); }
1460
+ }
1461
+
1462
+ /* Dark mode specific enhancements */
1463
+ .dark .glass-card {
1464
+ background: hsl(var(--background) / 0.4);
1465
+ border: 1px solid hsl(var(--border) / 0.2);
1466
+ }
1467
+
1468
+ .dark .gradient-mesh {
1469
+ background:
1470
+ radial-gradient(at 40% 20%, hsla(248, 73%, 70%, 0.15) 0px, transparent 50%),
1471
+ radial-gradient(at 80% 0%, hsla(280, 55%, 55%, 0.1) 0px, transparent 50%),
1472
+ radial-gradient(at 0% 50%, hsla(199, 89%, 48%, 0.1) 0px, transparent 50%),
1473
+ radial-gradient(at 80% 50%, hsla(340, 65%, 50%, 0.08) 0px, transparent 50%);
1474
+ }
1475
+
1476
+ /* Underline animation */
1477
+ .underline-animate {
1478
+ position: relative;
1479
+ }
1480
+
1481
+ .underline-animate::after {
1482
+ content: '';
1483
+ position: absolute;
1484
+ bottom: -2px;
1485
+ left: 0;
1486
+ width: 0;
1487
+ height: 2px;
1488
+ background: hsl(var(--primary));
1489
+ transition: width 0.3s ease;
1490
+ }
1491
+
1492
+ .underline-animate:hover::after {
1493
+ width: 100%;
1494
+ }
app/layout.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Inter, JetBrains_Mono, Plus_Jakarta_Sans } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const inter = Inter({
6
+ variable: "--font-sans",
7
+ subsets: ["latin"],
8
+ display: "swap",
9
+ preload: true,
10
+ });
11
+
12
+ const jetbrainsMono = JetBrains_Mono({
13
+ variable: "--font-mono",
14
+ subsets: ["latin"],
15
+ display: "swap",
16
+ });
17
+
18
+ const jakarta = Plus_Jakarta_Sans({
19
+ variable: "--font-heading",
20
+ subsets: ["latin"],
21
+ display: "swap",
22
+ preload: true,
23
+ });
24
+
25
+ export const metadata: Metadata = {
26
+ title: {
27
+ default: "NexaFlow — Digital Agency | Web & App Development",
28
+ template: "%s | NexaFlow",
29
+ },
30
+ description: "We design and build high-performance websites, mobile apps, and digital products that drive real business growth. Let's create something extraordinary.",
31
+ keywords: ["web development", "app development", "UI/UX design", "SEO", "digital agency", "nexaflow"],
32
+ authors: [{ name: "NexaFlow" }],
33
+ creator: "NexaFlow",
34
+ openGraph: {
35
+ type: "website",
36
+ locale: "en_US",
37
+ url: "https://nexaflow.dev",
38
+ siteName: "NexaFlow",
39
+ title: "NexaFlow — Digital Agency | Web & App Development",
40
+ description: "We design and build high-performance websites, mobile apps, and digital products that drive real business growth.",
41
+ },
42
+ twitter: {
43
+ card: "summary_large_image",
44
+ title: "NexaFlow — Digital Agency",
45
+ description: "We design and build high-performance websites, mobile apps, and digital products.",
46
+ },
47
+ robots: {
48
+ index: true,
49
+ follow: true,
50
+ },
51
+ };
52
+
53
+ import { SessionProvider } from "next-auth/react";
54
+ import { ThemeProvider } from "@/components/theme-provider";
55
+ import { Toaster } from "@/components/ui/sonner";
56
+
57
+ export default function RootLayout({
58
+ children,
59
+ }: Readonly<{
60
+ children: React.ReactNode;
61
+ }>) {
62
+ return (
63
+ <html lang="en" suppressHydrationWarning>
64
+ <body
65
+ className={`${inter.variable} ${jetbrainsMono.variable} ${jakarta.variable} font-sans antialiased`}
66
+ >
67
+ <ThemeProvider
68
+ attribute="class"
69
+ defaultTheme="system"
70
+ enableSystem
71
+ disableTransitionOnChange
72
+ >
73
+ <SessionProvider>
74
+ {children}
75
+ <Toaster />
76
+ </SessionProvider>
77
+ </ThemeProvider>
78
+ </body>
79
+ </html>
80
+ );
81
+ }
app/unauthorized/page.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link"
2
+ import { Button } from "@/components/ui/button"
3
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
4
+
5
+ export default function UnauthorizedPage() {
6
+ return (
7
+ <div className="flex min-h-screen items-center justify-center bg-muted/50 p-4">
8
+ <Card className="w-full max-w-md border-destructive/50">
9
+ <CardHeader>
10
+ <CardTitle className="text-2xl font-bold text-destructive">Access Denied</CardTitle>
11
+ <CardDescription>
12
+ You do not have permission to view this page.
13
+ </CardDescription>
14
+ </CardHeader>
15
+ <CardContent>
16
+ <p className="text-sm text-muted-foreground">
17
+ Please contact your administrator if you believe this is an error.
18
+ </p>
19
+ </CardContent>
20
+ <CardFooter className="flex justify-center gap-4">
21
+ <Button asChild variant="outline">
22
+ <Link href="/admin/dashboard">Return to Dashboard</Link>
23
+ </Button>
24
+ <Button asChild variant="default">
25
+ <Link href="/admin/login">Login with different account</Link>
26
+ </Button>
27
+ </CardFooter>
28
+ </Card>
29
+ </div>
30
+ )
31
+ }
components.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "registries": {}
23
+ }
components/admin/BlogForm.tsx ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useSession } from "next-auth/react";
6
+ import { useEditor, EditorContent } from '@tiptap/react';
7
+ import StarterKit from '@tiptap/starter-kit';
8
+ import LinkExtension from '@tiptap/extension-link';
9
+ import ImageExtension from '@tiptap/extension-image';
10
+ import { Button } from '@/components/ui/button';
11
+ import { Input } from '@/components/ui/input';
12
+ import { Label } from '@/components/ui/label';
13
+ import { Textarea } from '@/components/ui/textarea';
14
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
15
+ import { Card, CardContent } from '@/components/ui/card';
16
+ import { Bold, Italic, List, ListOrdered, Link as LinkIcon, Image as ImageIcon, Loader2 } from 'lucide-react';
17
+
18
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api';
19
+
20
+ interface BlogFormProps {
21
+ initialData?: any;
22
+ isEditing?: boolean;
23
+ }
24
+
25
+ export default function BlogForm({ initialData = null, isEditing = false }: BlogFormProps) {
26
+ const router = useRouter();
27
+ const { data: session } = useSession();
28
+ const [isLoading, setIsLoading] = useState(false);
29
+
30
+ // Form State
31
+ const [title, setTitle] = useState(initialData?.title || '');
32
+ const [slug, setSlug] = useState(initialData?.slug || '');
33
+ const [excerpt, setExcerpt] = useState(initialData?.excerpt || '');
34
+ const [featuredImage, setFeaturedImage] = useState(initialData?.featuredImage || '');
35
+ const [status, setStatus] = useState(initialData?.status || 'DRAFT');
36
+ const [publishedAt, setPublishedAt] = useState(initialData?.publishedAt ? new Date(initialData.publishedAt).toISOString().slice(0, 16) : '');
37
+ const [tags, setTags] = useState(initialData?.tags ? JSON.parse(initialData.tags).join(', ') : '');
38
+
39
+ const editor = useEditor({
40
+ extensions: [
41
+ StarterKit,
42
+ LinkExtension.configure({ openOnClick: false }),
43
+ ImageExtension.configure({ inline: true }),
44
+ ],
45
+ content: initialData?.content || '<p>Start writing your post here...</p>',
46
+ immediatelyRender: false,
47
+ editorProps: {
48
+ attributes: {
49
+ class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none min-h-[400px] p-4 border rounded-md max-w-none',
50
+ },
51
+ },
52
+ });
53
+
54
+ const setLink = () => {
55
+ const previousUrl = editor?.getAttributes('link').href;
56
+ const url = window.prompt('URL', previousUrl);
57
+ if (url === null) return;
58
+ if (url === '') {
59
+ editor?.chain().focus().extendMarkRange('link').unsetLink().run();
60
+ return;
61
+ }
62
+ editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
63
+ };
64
+
65
+ const addImage = () => {
66
+ const url = window.prompt('Image URL');
67
+ if (url) {
68
+ editor?.chain().focus().setImage({ src: url }).run();
69
+ }
70
+ };
71
+
72
+ const handleSubmit = async (e: React.FormEvent) => {
73
+ e.preventDefault();
74
+ setIsLoading(true);
75
+
76
+ const content = editor?.getHTML();
77
+ const tagsArray = tags.split(',').map((t: string) => t.trim()).filter(Boolean);
78
+
79
+ const payload = {
80
+ title,
81
+ slug: slug || undefined,
82
+ content,
83
+ excerpt,
84
+ featuredImage,
85
+ status,
86
+ publishedAt: publishedAt ? new Date(publishedAt).toISOString() : null,
87
+ tags: JSON.stringify(tagsArray)
88
+ };
89
+
90
+ try {
91
+ const url = isEditing && initialData?.id
92
+ ? `${API_URL}/admin/blog/${initialData.id}`
93
+ : `${API_URL}/admin/blog`;
94
+
95
+ const method = isEditing ? 'PUT' : 'POST';
96
+
97
+ const res = await fetch(url, {
98
+ method,
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ 'Authorization': `Bearer ${session?.user?.id}`
102
+ },
103
+ body: JSON.stringify(payload)
104
+ });
105
+
106
+ if (res.ok) {
107
+ router.push('/admin/blog');
108
+ router.refresh();
109
+ } else {
110
+ const data = await res.json();
111
+ alert(`Error: ${data.error || 'Failed to save post'}`);
112
+ }
113
+ } catch (error) {
114
+ console.error("Save error:", error);
115
+ alert("An unexpected error occurred while saving.");
116
+ } finally {
117
+ setIsLoading(false);
118
+ }
119
+ };
120
+
121
+ return (
122
+ <form onSubmit={handleSubmit} className="space-y-8">
123
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
124
+ <h2 className="text-3xl font-bold tracking-tight">
125
+ {isEditing ? 'Edit Article' : 'New Article'}
126
+ </h2>
127
+ <div className="flex gap-2 w-full md:w-auto">
128
+ <Button variant="outline" type="button" onClick={() => router.push('/admin/blog')}>
129
+ Cancel
130
+ </Button>
131
+ <Button type="submit" disabled={isLoading || !title || !editor?.getHTML()}>
132
+ {isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
133
+ {isEditing ? 'Save Changes' : 'Create Post'}
134
+ </Button>
135
+ </div>
136
+ </div>
137
+
138
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
139
+ {/* Main Content Area */}
140
+ <div className="lg:col-span-2 space-y-6">
141
+ <Card>
142
+ <CardContent className="p-6 space-y-4">
143
+ <div className="space-y-2">
144
+ <Label htmlFor="title">Post Title *</Label>
145
+ <Input
146
+ id="title"
147
+ value={title}
148
+ onChange={(e) => setTitle(e.target.value)}
149
+ placeholder="Enter an engaging title..."
150
+ required
151
+ className="text-lg font-medium"
152
+ />
153
+ </div>
154
+
155
+ <div className="space-y-2">
156
+ <Label>Content *</Label>
157
+ {editor && (
158
+ <div className="border rounded-md overflow-hidden bg-background">
159
+ {/* TipTap Toolbar */}
160
+ <div className="flex flex-wrap items-center gap-1 p-1 border-b bg-muted/30">
161
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive('bold') ? 'bg-muted' : ''}>
162
+ <Bold className="h-4 w-4" />
163
+ </Button>
164
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive('italic') ? 'bg-muted' : ''}>
165
+ <Italic className="h-4 w-4" />
166
+ </Button>
167
+ <div className="w-px h-4 bg-border mx-1" />
168
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive('heading', { level: 2 }) ? 'bg-muted' : ''}>
169
+ H2
170
+ </Button>
171
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={editor.isActive('heading', { level: 3 }) ? 'bg-muted' : ''}>
172
+ H3
173
+ </Button>
174
+ <div className="w-px h-4 bg-border mx-1" />
175
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive('bulletList') ? 'bg-muted' : ''}>
176
+ <List className="h-4 w-4" />
177
+ </Button>
178
+ <Button type="button" variant="ghost" size="sm" onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive('orderedList') ? 'bg-muted' : ''}>
179
+ <ListOrdered className="h-4 w-4" />
180
+ </Button>
181
+ <div className="w-px h-4 bg-border mx-1" />
182
+ <Button type="button" variant="ghost" size="sm" onClick={setLink} className={editor.isActive('link') ? 'bg-muted' : ''}>
183
+ <LinkIcon className="h-4 w-4" />
184
+ </Button>
185
+ <Button type="button" variant="ghost" size="sm" onClick={addImage}>
186
+ <ImageIcon className="h-4 w-4" />
187
+ </Button>
188
+ </div>
189
+ {/* TipTap Editor */}
190
+ <EditorContent editor={editor} />
191
+ </div>
192
+ )}
193
+ </div>
194
+
195
+ <div className="space-y-2">
196
+ <Label htmlFor="excerpt">Excerpt / Summary</Label>
197
+ <Textarea
198
+ id="excerpt"
199
+ value={excerpt}
200
+ onChange={(e) => setExcerpt(e.target.value)}
201
+ placeholder="Brief summary for search engines and social sharing..."
202
+ rows={3}
203
+ />
204
+ </div>
205
+ </CardContent>
206
+ </Card>
207
+ </div>
208
+
209
+ {/* Sidebar Metrics & Settings */}
210
+ <div className="space-y-6">
211
+ <Card>
212
+ <CardContent className="p-6 space-y-4">
213
+ <div className="space-y-2">
214
+ <Label htmlFor="status">Publishing Status</Label>
215
+ <Select value={status} onValueChange={setStatus}>
216
+ <SelectTrigger>
217
+ <SelectValue placeholder="Select status" />
218
+ </SelectTrigger>
219
+ <SelectContent>
220
+ <SelectItem value="DRAFT">Draft</SelectItem>
221
+ <SelectItem value="PUBLISHED">Published</SelectItem>
222
+ <SelectItem value="SCHEDULED">Scheduled</SelectItem>
223
+ </SelectContent>
224
+ </Select>
225
+ </div>
226
+
227
+ <div className="space-y-2">
228
+ <Label htmlFor="publishedAt">Publish Date</Label>
229
+ <Input
230
+ id="publishedAt"
231
+ type="datetime-local"
232
+ value={publishedAt}
233
+ onChange={(e) => setPublishedAt(e.target.value)}
234
+ disabled={status === 'DRAFT'}
235
+ />
236
+ {status === 'DRAFT' && <p className="text-xs text-muted-foreground">Date is disabled while in Draft status.</p>}
237
+ </div>
238
+
239
+ <div className="space-y-2 pt-4 border-t">
240
+ <Label htmlFor="slug">Custom URL Slug (Optional)</Label>
241
+ <Input
242
+ id="slug"
243
+ value={slug}
244
+ onChange={(e) => setSlug(e.target.value)}
245
+ placeholder="e.g. my-awesome-post"
246
+ />
247
+ <p className="text-xs text-muted-foreground">Leave blank to auto-generate from title.</p>
248
+ </div>
249
+
250
+ <div className="space-y-2 pt-4 border-t">
251
+ <Label htmlFor="featuredImage">Featured Image URL</Label>
252
+ <Input
253
+ id="featuredImage"
254
+ value={featuredImage}
255
+ onChange={(e) => setFeaturedImage(e.target.value)}
256
+ placeholder="https://..."
257
+ />
258
+ {featuredImage && (
259
+ <div className="mt-2 text-xs text-muted-foreground overflow-hidden rounded-md h-32 bg-muted relative">
260
+ {/* eslint-disable-next-line @next/next/no-img-element */}
261
+ <img src={featuredImage} alt="Featured preview" className="object-cover w-full h-full" />
262
+ </div>
263
+ )}
264
+ </div>
265
+
266
+ <div className="space-y-2 pt-4 border-t">
267
+ <Label htmlFor="tags">Tags (Comma separated)</Label>
268
+ <Input
269
+ id="tags"
270
+ value={tags}
271
+ onChange={(e) => setTags(e.target.value)}
272
+ placeholder="React, Design, Tech"
273
+ />
274
+ </div>
275
+ </CardContent>
276
+ </Card>
277
+ </div>
278
+ </div>
279
+ </form>
280
+ );
281
+ }
components/auth/RoleGuard.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useSession } from "next-auth/react"
4
+ import { useRouter } from "next/navigation"
5
+ import { useEffect } from "react"
6
+ import { RoleType } from "@/types/auth"
7
+
8
+ interface RoleGuardProps {
9
+ children: React.ReactNode
10
+ allowedRoles: RoleType[]
11
+ }
12
+
13
+ export function RoleGuard({ children, allowedRoles }: RoleGuardProps) {
14
+ const { data: session, status } = useSession()
15
+ const router = useRouter()
16
+
17
+ useEffect(() => {
18
+ if (status === "loading") return
19
+
20
+ if (!session) {
21
+ router.push("/admin/login")
22
+ return
23
+ }
24
+
25
+ const userRole = (session.user as any).role as RoleType
26
+
27
+ if (!allowedRoles.includes(userRole)) {
28
+ router.push("/unauthorized")
29
+ }
30
+ }, [session, status, router, allowedRoles])
31
+
32
+ if (status === "loading") {
33
+ return <div>Loading...</div>
34
+ }
35
+
36
+ if (!session) {
37
+ return null
38
+ }
39
+
40
+ const userRole = (session.user as any).role as RoleType
41
+ if (!allowedRoles.includes(userRole)) {
42
+ return null
43
+ }
44
+
45
+ return <>{children}</>
46
+ }
components/home/CTASection.tsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Button } from "@/components/ui/button"
5
+ import { ArrowRight, Sparkles, Zap, Rocket, MessageCircle } from "lucide-react"
6
+ import { useRef, useState, useEffect } from "react"
7
+
8
+ // Pre-computed particle data to avoid hydration mismatch
9
+ const CTA_PARTICLES = [
10
+ // Left side particles
11
+ { left: 3.5, top: 15.2, delay: 2.1, duration: 19.4 },
12
+ { left: 8.7, top: 45.8, delay: 7.6, duration: 21.8 },
13
+ { left: 5.2, top: 78.3, delay: 11.2, duration: 18.2 },
14
+ { left: 12.4, top: 32.5, delay: 4.8, duration: 22.6 },
15
+ { left: 6.8, top: 62.9, delay: 9.3, duration: 17.5 },
16
+
17
+ // Right side particles
18
+ { left: 91.3, top: 18.6, delay: 5.4, duration: 20.3 },
19
+ { left: 86.5, top: 52.4, delay: 10.1, duration: 18.9 },
20
+ { left: 94.7, top: 85.2, delay: 3.7, duration: 22.1 },
21
+ { left: 88.2, top: 38.7, delay: 8.4, duration: 19.6 },
22
+ { left: 92.8, top: 68.4, delay: 12.9, duration: 21.2 },
23
+
24
+ // Top edge
25
+ { left: 25.4, top: 5.8, delay: 6.2, duration: 18.4 },
26
+ { left: 55.7, top: 3.2, delay: 1.5, duration: 22.8 },
27
+ { left: 72.1, top: 8.4, delay: 9.8, duration: 17.9 },
28
+
29
+ // Bottom edge
30
+ { left: 18.9, top: 92.6, delay: 4.3, duration: 20.7 },
31
+ { left: 48.3, top: 95.1, delay: 11.7, duration: 19.2 },
32
+ { left: 68.5, top: 89.3, delay: 7.1, duration: 21.5 },
33
+
34
+ // Corners
35
+ { left: 2.1, top: 2.4, delay: 13.5, duration: 18.6 },
36
+ { left: 97.8, top: 4.7, delay: 0.8, duration: 22.4 },
37
+ { left: 1.5, top: 96.2, delay: 5.6, duration: 19.8 },
38
+ { left: 98.4, top: 94.5, delay: 10.4, duration: 17.3 },
39
+ ]
40
+
41
+ export function CTASection() {
42
+ const sectionRef = useRef<HTMLElement>(null)
43
+ const [isVisible, setIsVisible] = useState(false)
44
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
45
+
46
+ useEffect(() => {
47
+ const observer = new IntersectionObserver(
48
+ ([entry]) => {
49
+ if (entry.isIntersecting) {
50
+ setIsVisible(true)
51
+ }
52
+ },
53
+ { threshold: 0.2 }
54
+ )
55
+
56
+ if (sectionRef.current) {
57
+ observer.observe(sectionRef.current)
58
+ }
59
+
60
+ return () => observer.disconnect()
61
+ }, [])
62
+
63
+ useEffect(() => {
64
+ const handleMouseMove = (e: MouseEvent) => {
65
+ if (sectionRef.current) {
66
+ const rect = sectionRef.current.getBoundingClientRect()
67
+ setMousePosition({
68
+ x: (e.clientX - rect.left) / rect.width,
69
+ y: (e.clientY - rect.top) / rect.height,
70
+ })
71
+ }
72
+ }
73
+
74
+ window.addEventListener('mousemove', handleMouseMove)
75
+ return () => window.removeEventListener('mousemove', handleMouseMove)
76
+ }, [])
77
+
78
+ return (
79
+ <section ref={sectionRef} className="relative py-24 overflow-hidden bg-background" aria-label="Call to action">
80
+ {/* Dynamic gradient mesh */}
81
+ <div className="absolute inset-0 gradient-mesh opacity-30" />
82
+
83
+ {/* Animated orbs */}
84
+ <div
85
+ className="absolute w-[600px] h-[600px] rounded-full opacity-20 blur-3xl pointer-events-none transition-transform duration-1000"
86
+ style={{
87
+ background: 'radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)',
88
+ top: '-20%',
89
+ right: '-10%',
90
+ transform: `translate(${mousePosition.x * 50}px, ${mousePosition.y * 50}px)`,
91
+ }}
92
+ />
93
+ <div
94
+ className="absolute w-[500px] h-[500px] rounded-full opacity-15 blur-3xl pointer-events-none transition-transform duration-1000"
95
+ style={{
96
+ background: 'radial-gradient(circle, hsl(280, 65%, 60%) 0%, transparent 70%)',
97
+ bottom: '-20%',
98
+ left: '-10%',
99
+ transform: `translate(${-mousePosition.x * 30}px, ${-mousePosition.y * 30}px)`,
100
+ }}
101
+ />
102
+
103
+ {/* Animated particles */}
104
+ <div className="absolute inset-0 pointer-events-none overflow-hidden">
105
+ {CTA_PARTICLES.map((particle, i) => (
106
+ <div
107
+ key={i}
108
+ className="particle"
109
+ style={{
110
+ left: `${particle.left}%`,
111
+ top: `${particle.top}%`,
112
+ animationDelay: `${particle.delay}s`,
113
+ animationDuration: `${particle.duration}s`,
114
+ }}
115
+ />
116
+ ))}
117
+ </div>
118
+
119
+ {/* Grid pattern */}
120
+ <div className="absolute inset-0 opacity-5">
121
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,hsl(var(--primary))_1px,transparent_1px)] bg-size-[48px_48px]" />
122
+ </div>
123
+
124
+ <div className="container relative z-10 flex flex-col items-center text-center">
125
+ <div
126
+ className={`max-w-4xl transition-all duration-1000 ${
127
+ isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-20'
128
+ }`}
129
+ >
130
+ {/* Main CTA card with animated border */}
131
+ <div className="relative">
132
+ {/* Animated gradient border */}
133
+ <div className="absolute -inset-1 rounded-[2rem] animated-border opacity-50" />
134
+
135
+ <div className="relative glass-card rounded-[2rem] p-12 md:p-16 overflow-hidden">
136
+ {/* Inner glow */}
137
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-accent/10 opacity-50" />
138
+
139
+ {/* Floating icons */}
140
+ <div className="absolute top-8 left-8 animate-float opacity-20" style={{ animationDelay: '0s' }}>
141
+ <Rocket className="w-8 h-8 text-primary" suppressHydrationWarning />
142
+ </div>
143
+ <div className="absolute top-12 right-12 animate-float opacity-20" style={{ animationDelay: '1s' }}>
144
+ <Zap className="w-6 h-6 text-amber-500" suppressHydrationWarning />
145
+ </div>
146
+ <div className="absolute bottom-8 left-16 animate-float opacity-20" style={{ animationDelay: '2s' }}>
147
+ <MessageCircle className="w-7 h-7 text-cyan-500" suppressHydrationWarning />
148
+ </div>
149
+
150
+ <div className="relative z-10">
151
+ {/* Badge */}
152
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card mb-8">
153
+ <Sparkles className="w-4 h-4 text-primary animate-pulse" suppressHydrationWarning />
154
+ <span className="text-sm font-medium">Let&apos;s Create Together</span>
155
+ </div>
156
+
157
+ <h2 className="text-4xl md:text-5xl lg:text-6xl mb-8 text-foreground font-bold tracking-tight">
158
+ Ready to Start Your{" "}
159
+ <span className="gradient-text relative">
160
+ Project
161
+ <svg className="absolute -bottom-2 left-0 w-full" viewBox="0 0 200 12" fill="none">
162
+ <path d="M2 10C50 4 150 2 198 10" stroke="hsl(var(--primary))" strokeWidth="3" strokeLinecap="round" style={{
163
+ strokeDasharray: '200',
164
+ strokeDashoffset: isVisible ? '0' : '200',
165
+ transition: 'stroke-dashoffset 1s ease 0.5s'
166
+ }} />
167
+ </svg>
168
+ </span>
169
+ ?
170
+ </h2>
171
+
172
+ <p className="text-lg md:text-xl mb-12 max-w-2xl text-muted-foreground leading-relaxed mx-auto">
173
+ Let&apos;s build something{" "}
174
+ <span className="text-primary font-medium">amazing</span> together.
175
+ Contact us today for a free consultation and a detailed project quote.
176
+ </p>
177
+
178
+ <div className="flex flex-col sm:flex-row gap-4 justify-center">
179
+ <Button
180
+ asChild
181
+ size="xl"
182
+ className="group h-14 px-10 text-lg rounded-2xl shine-effect relative overflow-hidden"
183
+ >
184
+ <Link href="/quote">
185
+ <span className="relative z-10 flex items-center">
186
+ Get a Free Quote
187
+ <ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1 group-hover:scale-110" suppressHydrationWarning />
188
+ </span>
189
+ </Link>
190
+ </Button>
191
+ <Button
192
+ asChild
193
+ size="xl"
194
+ variant="outline"
195
+ className="group h-14 px-10 text-lg rounded-2xl glass-card hover:bg-primary/10"
196
+ >
197
+ <Link href="/contact">
198
+ <MessageCircle className="mr-2 h-5 w-5 group-hover:rotate-12 transition-transform" suppressHydrationWarning />
199
+ Contact Us
200
+ </Link>
201
+ </Button>
202
+ </div>
203
+
204
+ {/* Trust badges */}
205
+ <div className="flex items-center justify-center gap-6 mt-10 text-sm text-muted-foreground">
206
+ <div className="flex items-center gap-2">
207
+ <Zap className="w-4 h-4 text-amber-500" suppressHydrationWarning />
208
+ <span>Fast Response</span>
209
+ </div>
210
+ <div className="w-1 h-1 rounded-full bg-muted-foreground/50" />
211
+ <div className="flex items-center gap-2">
212
+ <Rocket className="w-4 h-4 text-primary" suppressHydrationWarning />
213
+ <span>Free Consultation</span>
214
+ </div>
215
+ <div className="w-1 h-1 rounded-full bg-muted-foreground/50" />
216
+ <div className="flex items-center gap-2">
217
+ <Sparkles className="w-4 h-4 text-cyan-500" suppressHydrationWarning />
218
+ <span>Custom Solutions</span>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ {/* Shine effect */}
224
+ <div className="absolute inset-0 shine-effect pointer-events-none" />
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </section>
230
+ )
231
+ }
components/home/FeaturedPortfolio.tsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
6
+ import { ExternalLink, Eye, ArrowUpRight } from "lucide-react"
7
+ import { useRef, useState, useEffect } from "react"
8
+
9
+ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
10
+
11
+ interface Project {
12
+ title: string
13
+ slug: string
14
+ category: string
15
+ tags: string[]
16
+ imageUrl: string
17
+ gradient: string
18
+ emoji: string
19
+ }
20
+
21
+ async function getFeaturedProjects(): Promise<Project[]> {
22
+ try {
23
+ const res = await fetch(`${API_URL}/portfolio`, { cache: 'no-store' })
24
+ if (!res.ok) return []
25
+ const data = await res.json()
26
+
27
+ return data.slice(0, 3).map((item: any) => ({
28
+ title: item.title,
29
+ slug: item.slug,
30
+ category: item.industry || "General",
31
+ tags: item.technologies ? item.technologies.split(',').map((t: string) => t.trim()) : [],
32
+ imageUrl: item.imageUrl,
33
+ gradient: "from-blue-600 to-cyan-500",
34
+ emoji: "✨",
35
+ }))
36
+ } catch (e) {
37
+ console.error("Failed to fetch featured projects", e)
38
+ return []
39
+ }
40
+ }
41
+
42
+ function PortfolioCard({ project, index }: { project: Project; index: number }) {
43
+ const cardRef = useRef<HTMLDivElement>(null)
44
+ const [isVisible, setIsVisible] = useState(false)
45
+ const [isHovered, setIsHovered] = useState(false)
46
+
47
+ useEffect(() => {
48
+ const observer = new IntersectionObserver(
49
+ ([entry]) => {
50
+ if (entry.isIntersecting) {
51
+ setIsVisible(true)
52
+ }
53
+ },
54
+ { threshold: 0.1 }
55
+ )
56
+
57
+ if (cardRef.current) {
58
+ observer.observe(cardRef.current)
59
+ }
60
+
61
+ return () => observer.disconnect()
62
+ }, [])
63
+
64
+ return (
65
+ <article
66
+ ref={cardRef}
67
+ className={`transition-all duration-700 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-20'}`}
68
+ style={{ transitionDelay: `${index * 150}ms` }}
69
+ >
70
+ <Link href={`/portfolio/${project.slug}`} className="block h-full">
71
+ <Card
72
+ className="group relative h-full overflow-hidden glass-card cursor-pointer transition-all duration-500 rounded-3xl border-0"
73
+ onMouseEnter={() => setIsHovered(true)}
74
+ onMouseLeave={() => setIsHovered(false)}
75
+ >
76
+ {/* Animated background gradient */}
77
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
78
+
79
+ {/* Thumbnail with overlay effect */}
80
+ <div className="relative aspect-16/10 overflow-hidden">
81
+ {project.imageUrl ? (
82
+ <img
83
+ src={project.imageUrl.startsWith('http') || project.imageUrl.startsWith('/') ? project.imageUrl : `/${project.imageUrl}`}
84
+ alt={project.title}
85
+ className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
86
+ />
87
+ ) : (
88
+ <div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/20 to-accent/20">
89
+ <span className="text-6xl group-hover:scale-110 transition-transform duration-300" role="img" aria-label={project.title}>
90
+ {project.emoji || '🚀'}
91
+ </span>
92
+ </div>
93
+ )}
94
+
95
+ {/* Overlay gradient */}
96
+ <div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent opacity-60 group-hover:opacity-40 transition-opacity duration-500" />
97
+
98
+ {/* Hover overlay with pattern */}
99
+ <div className="absolute inset-0 bg-primary/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex items-center justify-center">
100
+ <div className="w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center transform scale-0 group-hover:scale-100 transition-transform duration-500">
101
+ <Eye className="w-5 h-5 text-white" suppressHydrationWarning />
102
+ </div>
103
+ </div>
104
+
105
+ {/* Category badge */}
106
+ <div className="absolute top-4 left-4">
107
+ <span className="px-3 py-1.5 rounded-full text-xs font-semibold uppercase tracking-wider glass-card">
108
+ {project.category}
109
+ </span>
110
+ </div>
111
+ </div>
112
+
113
+ <CardHeader className="relative pb-3">
114
+ <h3 className="text-xl font-bold leading-tight group-hover:text-primary transition-colors duration-300 flex items-center gap-2">
115
+ {project.title}
116
+ <ArrowUpRight className="w-4 h-4 opacity-0 -translate-x-2 -translate-y-2 group-hover:opacity-100 group-hover:translate-x-0 group-hover:translate-y-0 transition-all duration-300 text-primary" suppressHydrationWarning />
117
+ </h3>
118
+ </CardHeader>
119
+
120
+ <CardContent>
121
+ <div className="flex flex-wrap gap-2">
122
+ {project.tags.slice(0, 4).map((tag, tagIndex) => (
123
+ <span
124
+ key={tag}
125
+ className="px-3 py-1.5 rounded-full text-xs font-medium glass-card transition-all duration-300 hover:scale-105 hover:bg-primary/20"
126
+ style={{ transitionDelay: `${tagIndex * 50}ms` }}
127
+ >
128
+ {tag}
129
+ </span>
130
+ ))}
131
+ {project.tags.length > 4 && (
132
+ <span className="px-3 py-1.5 rounded-full text-xs font-medium text-primary">
133
+ +{project.tags.length - 4} more
134
+ </span>
135
+ )}
136
+ </div>
137
+ </CardContent>
138
+
139
+ {/* Bottom accent line */}
140
+ <div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary via-accent to-primary transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left" />
141
+
142
+ {/* Shine effect */}
143
+ <div className="absolute inset-0 shine-effect pointer-events-none rounded-3xl" />
144
+ </Card>
145
+ </Link>
146
+ </article>
147
+ )
148
+ }
149
+
150
+ export function FeaturedPortfolioWrapper() {
151
+ const [projects, setProjects] = useState<Project[]>([])
152
+ const [isVisible, setIsVisible] = useState(false)
153
+ const sectionRef = useRef<HTMLElement>(null)
154
+
155
+ useEffect(() => {
156
+ getFeaturedProjects().then(setProjects)
157
+ }, [])
158
+
159
+ useEffect(() => {
160
+ const observer = new IntersectionObserver(
161
+ ([entry]) => {
162
+ if (entry.isIntersecting) {
163
+ setIsVisible(true)
164
+ }
165
+ },
166
+ { threshold: 0.1 }
167
+ )
168
+
169
+ if (sectionRef.current) {
170
+ observer.observe(sectionRef.current)
171
+ }
172
+
173
+ return () => observer.disconnect()
174
+ }, [])
175
+
176
+ return (
177
+ <section ref={sectionRef} className="py-24 bg-muted/20 relative overflow-hidden" id="portfolio">
178
+ {/* Background decorations */}
179
+ <div className="absolute top-1/2 left-0 w-72 h-72 bg-primary/10 rounded-full blur-3xl -translate-y-1/2" />
180
+ <div className="absolute top-1/4 right-0 w-96 h-96 bg-accent/10 rounded-full blur-3xl" />
181
+
182
+ {/* Animated grid pattern */}
183
+ <div className="absolute inset-0 opacity-5">
184
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--primary))_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--primary))_1px,transparent_1px)] bg-size-[48px_48px]" />
185
+ </div>
186
+
187
+ <div className="container relative z-10">
188
+ {/* Section header */}
189
+ <div className={`flex flex-col md:flex-row justify-between items-start md:items-end mb-14 gap-6 transition-all duration-1000 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}>
190
+ <div className="max-w-2xl">
191
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card mb-6">
192
+ <span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
193
+ <span className="text-sm font-medium text-primary">Portfolio</span>
194
+ </div>
195
+
196
+ <h2 className="text-4xl md:text-5xl mb-6 font-bold tracking-tight">
197
+ Featured <span className="gradient-text">Work</span>
198
+ </h2>
199
+
200
+ <p className="text-lg text-muted-foreground leading-relaxed">
201
+ Explore our latest projects and the impact we&apos;ve made for our clients.
202
+ </p>
203
+ </div>
204
+
205
+ <Button asChild variant="outline" className="group glass-card hover:bg-primary/10 shrink-0">
206
+ <Link href="/portfolio">
207
+ View All Projects
208
+ <ExternalLink className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" suppressHydrationWarning />
209
+ </Link>
210
+ </Button>
211
+ </div>
212
+
213
+ {/* Projects grid */}
214
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
215
+ {projects.map((project, i) => (
216
+ <PortfolioCard key={project.slug} project={project} index={i} />
217
+ ))}
218
+ </div>
219
+
220
+ {/* Empty state */}
221
+ {projects.length === 0 && (
222
+ <div className="text-center py-16">
223
+ <p className="text-muted-foreground">No projects to display yet.</p>
224
+ </div>
225
+ )}
226
+
227
+ {/* Bottom stats */}
228
+ <div className={`mt-16 flex justify-center gap-8 md:gap-16 transition-all duration-1000 delay-500 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}>
229
+ {[
230
+ { value: "50+", label: "Projects Completed" },
231
+ { value: "30+", label: "Happy Clients" },
232
+ { value: "5+", label: "Years Experience" },
233
+ ].map((stat) => (
234
+ <div key={stat.label} className="text-center group cursor-default">
235
+ <div className="text-2xl md:text-3xl font-bold gradient-text group-hover:scale-110 transition-transform duration-300">
236
+ {stat.value}
237
+ </div>
238
+ <div className="text-xs md:text-sm text-muted-foreground mt-1">
239
+ {stat.label}
240
+ </div>
241
+ </div>
242
+ ))}
243
+ </div>
244
+ </div>
245
+ </section>
246
+ )
247
+ }
248
+
249
+ // Export the wrapped component as the default
250
+ export function FeaturedPortfolio() {
251
+ return <FeaturedPortfolioWrapper />
252
+ }
components/home/HeroSection.tsx ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Button } from "@/components/ui/button"
5
+ import { ArrowRight, Sparkles, Code2, Rocket, Zap } from "lucide-react"
6
+ import { useEffect, useRef, useState } from "react"
7
+
8
+ // Pre-computed particle data to avoid hydration mismatch
9
+ // These values are static and will be the same on server and client
10
+ const PARTICLES = [
11
+ // Top-left quadrant
12
+ { left: 5.2, top: 8.7, delay: 2.4, duration: 18.6, width: 5.8, height: 5.2, opacity: 0.32 },
13
+ { left: 12.8, top: 15.3, delay: 7.1, duration: 21.2, width: 4.5, height: 6.8, opacity: 0.28 },
14
+ { left: 8.4, top: 28.9, delay: 11.5, duration: 19.8, width: 6.2, height: 4.9, opacity: 0.35 },
15
+ { left: 18.6, top: 6.2, delay: 4.8, duration: 22.4, width: 5.1, height: 5.7, opacity: 0.30 },
16
+ { left: 3.9, top: 42.5, delay: 9.3, duration: 17.5, width: 4.8, height: 6.3, opacity: 0.26 },
17
+ { left: 15.3, top: 35.8, delay: 14.7, duration: 20.1, width: 6.5, height: 5.4, opacity: 0.33 },
18
+
19
+ // Top-right quadrant
20
+ { left: 82.4, top: 5.8, delay: 5.2, duration: 19.4, width: 5.6, height: 6.1, opacity: 0.31 },
21
+ { left: 91.7, top: 18.4, delay: 10.8, duration: 22.8, width: 4.2, height: 5.5, opacity: 0.27 },
22
+ { left: 76.3, top: 12.6, delay: 3.1, duration: 18.2, width: 6.8, height: 4.6, opacity: 0.34 },
23
+ { left: 88.5, top: 32.9, delay: 8.9, duration: 21.6, width: 5.3, height: 6.7, opacity: 0.29 },
24
+ { left: 94.2, top: 8.3, delay: 13.4, duration: 17.9, width: 4.9, height: 5.8, opacity: 0.32 },
25
+
26
+ // Bottom-left quadrant
27
+ { left: 6.8, top: 72.4, delay: 6.5, duration: 20.3, width: 5.9, height: 5.1, opacity: 0.28 },
28
+ { left: 14.2, top: 85.6, delay: 12.1, duration: 18.7, width: 4.6, height: 6.4, opacity: 0.33 },
29
+ { left: 9.7, top: 58.3, delay: 1.8, duration: 22.1, width: 6.1, height: 4.8, opacity: 0.30 },
30
+ { left: 22.5, top: 91.8, delay: 9.6, duration: 19.5, width: 5.4, height: 5.9, opacity: 0.26 },
31
+
32
+ // Bottom-right quadrant
33
+ { left: 85.3, top: 78.2, delay: 4.3, duration: 21.8, width: 4.7, height: 6.2, opacity: 0.31 },
34
+ { left: 92.8, top: 65.4, delay: 11.2, duration: 18.4, width: 5.2, height: 5.6, opacity: 0.29 },
35
+ { left: 78.6, top: 88.9, delay: 7.8, duration: 20.7, width: 6.4, height: 4.5, opacity: 0.34 },
36
+ { left: 96.1, top: 82.5, delay: 2.9, duration: 22.6, width: 5.8, height: 6.1, opacity: 0.27 },
37
+
38
+ // Center spread (top area only to avoid stat cards)
39
+ { left: 35.4, top: 12.8, delay: 8.4, duration: 19.1, width: 5.5, height: 5.3, opacity: 0.30 },
40
+ { left: 48.7, top: 8.2, delay: 3.6, duration: 21.4, width: 4.4, height: 6.6, opacity: 0.28 },
41
+ { left: 62.3, top: 15.9, delay: 10.2, duration: 18.9, width: 6.3, height: 5.0, opacity: 0.32 },
42
+ { left: 55.8, top: 5.4, delay: 6.1, duration: 22.2, width: 5.1, height: 5.8, opacity: 0.29 },
43
+ { left: 41.2, top: 22.6, delay: 12.8, duration: 17.8, width: 5.7, height: 4.7, opacity: 0.31 },
44
+ { left: 68.9, top: 10.3, delay: 5.4, duration: 20.5, width: 4.9, height: 6.0, opacity: 0.33 },
45
+ { left: 29.6, top: 3.8, delay: 9.7, duration: 21.9, width: 6.0, height: 5.2, opacity: 0.27 },
46
+ { left: 72.4, top: 18.5, delay: 2.2, duration: 19.3, width: 5.3, height: 5.5, opacity: 0.30 },
47
+ { left: 45.1, top: 25.7, delay: 7.5, duration: 18.1, width: 4.6, height: 6.3, opacity: 0.28 },
48
+ { left: 58.6, top: 2.9, delay: 11.8, duration: 22.7, width: 6.2, height: 4.8, opacity: 0.32 },
49
+ ]
50
+
51
+ export function HeroSection() {
52
+ const heroRef = useRef<HTMLDivElement>(null)
53
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
54
+
55
+ useEffect(() => {
56
+ const handleMouseMove = (e: MouseEvent) => {
57
+ if (heroRef.current) {
58
+ const rect = heroRef.current.getBoundingClientRect()
59
+ setMousePosition({
60
+ x: (e.clientX - rect.left) / rect.width,
61
+ y: (e.clientY - rect.top) / rect.height,
62
+ })
63
+ }
64
+ }
65
+
66
+ window.addEventListener('mousemove', handleMouseMove)
67
+ return () => window.removeEventListener('mousemove', handleMouseMove)
68
+ }, [])
69
+
70
+ return (
71
+ <section ref={heroRef} className="relative overflow-hidden pt-20 pb-32 md:pt-28 lg:pt-36 xl:pt-44 bg-background min-h-screen flex items-center">
72
+ {/* Animated gradient mesh background */}
73
+ <div className="absolute inset-0 gradient-mesh opacity-40" />
74
+
75
+ {/* Noise overlay for depth */}
76
+ <div className="absolute inset-0 noise-overlay" />
77
+
78
+ {/* Morphing blob 1 */}
79
+ <div
80
+ className="absolute w-[500px] h-[500px] morphing-blob opacity-30 blur-3xl pointer-events-none"
81
+ style={{
82
+ background: `linear-gradient(135deg, hsl(var(--primary)), hsl(280, 65%, 60%))`,
83
+ top: '10%',
84
+ right: '5%',
85
+ transform: `translate(${mousePosition.x * 30}px, ${mousePosition.y * 30}px)`,
86
+ transition: 'transform 0.3s ease-out',
87
+ }}
88
+ />
89
+
90
+ {/* Morphing blob 2 */}
91
+ <div
92
+ className="absolute w-[400px] h-[400px] morphing-blob opacity-20 blur-3xl pointer-events-none"
93
+ style={{
94
+ background: `linear-gradient(135deg, hsl(280, 65%, 60%), hsl(199, 89%, 48%))`,
95
+ bottom: '10%',
96
+ left: '5%',
97
+ transform: `translate(${-mousePosition.x * 20}px, ${-mousePosition.y * 20}px)`,
98
+ transition: 'transform 0.3s ease-out',
99
+ animationDelay: '-5s',
100
+ }}
101
+ />
102
+
103
+ {/* Floating particles - behind all content */}
104
+ <div className="absolute inset-0 pointer-events-none overflow-hidden -z-10">
105
+ {PARTICLES.map((particle, i) => (
106
+ <div
107
+ key={i}
108
+ className="particle"
109
+ style={{
110
+ left: `${particle.left}%`,
111
+ top: `${particle.top}%`,
112
+ animationDelay: `${particle.delay}s`,
113
+ animationDuration: `${particle.duration}s`,
114
+ width: `${particle.width}px`,
115
+ height: `${particle.height}px`,
116
+ opacity: particle.opacity,
117
+ }}
118
+ />
119
+ ))}
120
+ </div>
121
+
122
+ {/* Aurora effect */}
123
+ <div className="absolute inset-0 aurora-bg opacity-30" />
124
+
125
+ {/* Subtle grid pattern */}
126
+ <div className="absolute inset-0 -z-10 opacity-10">
127
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,hsl(var(--muted))_1px,transparent_1px)] bg-size-[32px_32px]" />
128
+ </div>
129
+
130
+ <div className="container relative z-10 flex flex-col items-center text-center">
131
+ {/* Animated badge */}
132
+ <div className="animate-fade-in-up mb-8 inline-flex items-center gap-2 rounded-full glass-card px-6 py-3 text-sm font-medium transition-all duration-500 hover:scale-105 hover:shadow-lg group cursor-default">
133
+ <span className="relative flex h-2 w-2">
134
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
135
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
136
+ </span>
137
+ <Sparkles className="h-4 w-4 text-primary animate-pulse" suppressHydrationWarning />
138
+ <span className="interactive-underline cursor-pointer">Your Vision, Our Craft</span>
139
+ </div>
140
+
141
+ {/* H1 with creative styling */}
142
+ <h1 className="animate-fade-in-up animate-delay-1 mb-8 text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-extrabold tracking-tight leading-tight">
143
+ <span className="block">We Design &amp; Build</span>
144
+ <span className="relative inline-block mt-2">
145
+ <span className="gradient-text">Digital Products</span>
146
+ <svg className="absolute -bottom-2 left-0 w-full" viewBox="0 0 300 12" fill="none">
147
+ <path d="M2 10C50 4 150 2 298 10" stroke="hsl(var(--primary))" strokeWidth="3" strokeLinecap="round" className="animate-dash" style={{
148
+ strokeDasharray: '300',
149
+ strokeDashoffset: '300',
150
+ animation: 'dash 2s ease forwards 0.5s'
151
+ }} />
152
+ </svg>
153
+ </span>
154
+ <span className="block mt-2 wave-text">
155
+ <span>T</span><span>h</span><span>a</span><span>t</span>
156
+ <span className="mx-2"> </span>
157
+ <span className="text-primary neon-glow">GROW</span>
158
+ </span>
159
+ </h1>
160
+
161
+ {/* Subheadline */}
162
+ <p className="animate-fade-in-up animate-delay-2 mb-12 max-w-2xl text-lg sm:text-xl text-muted-foreground leading-relaxed">
163
+ From stunning websites to powerful mobile apps — we turn ambitious ideas
164
+ into high-performance digital experiences that your users will{" "}
165
+ <span className="text-primary font-medium">love</span>.
166
+ </p>
167
+
168
+ {/* Enhanced CTAs */}
169
+ <div className="animate-fade-in-up animate-delay-3 flex flex-col gap-4 sm:flex-row">
170
+ <Button asChild size="xl" className="group relative overflow-hidden shine-effect ripple-button">
171
+ <Link href="/quote">
172
+ <span className="relative z-10 flex items-center">
173
+ Get Started
174
+ <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" suppressHydrationWarning />
175
+ </span>
176
+ </Link>
177
+ </Button>
178
+ <Button asChild variant="outline" size="xl" className="group glass-card hover:bg-primary/10">
179
+ <Link href="/portfolio">
180
+ <span className="flex items-center">
181
+ View Portfolio
182
+ <Rocket className="ml-2 h-4 w-4 transition-transform group-hover:rotate-12" suppressHydrationWarning />
183
+ </span>
184
+ </Link>
185
+ </Button>
186
+ </div>
187
+
188
+ {/* Floating tech icons */}
189
+ <div className="absolute top-1/4 left-10 hidden xl:block animate-float" style={{ animationDelay: '0s' }}>
190
+ <div className="glass-card p-3 rounded-xl">
191
+ <Code2 className="h-6 w-6 text-primary" suppressHydrationWarning />
192
+ </div>
193
+ </div>
194
+ <div className="absolute top-1/3 right-10 hidden xl:block animate-float" style={{ animationDelay: '1s' }}>
195
+ <div className="glass-card p-3 rounded-xl">
196
+ <Zap className="h-6 w-6 text-amber-500" suppressHydrationWarning />
197
+ </div>
198
+ </div>
199
+ <div className="absolute bottom-1/3 left-20 hidden xl:block animate-float" style={{ animationDelay: '2s' }}>
200
+ <div className="glass-card p-3 rounded-xl">
201
+ <Rocket className="h-6 w-6 text-cyan-500" suppressHydrationWarning />
202
+ </div>
203
+ </div>
204
+
205
+ {/* Key Statistics with enhanced styling */}
206
+ <div className="animate-fade-in-up animate-delay-5 mt-24 relative">
207
+ {/* Background overlay to hide particles behind cards */}
208
+ <div className="absolute inset-0 bg-gradient-to-t from-background via-background/95 to-background/80 -z-10 rounded-3xl" />
209
+
210
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-6 md:gap-8 w-full max-w-4xl">
211
+ {[
212
+ { value: "50+", label: "Projects Shipped", icon: Rocket },
213
+ { value: "98%", label: "Client Satisfaction", icon: Sparkles },
214
+ { value: "5+", label: "Years of Expertise", icon: Zap },
215
+ ].map((stat, i) => (
216
+ <div
217
+ key={stat.label}
218
+ className="group relative text-center cursor-default"
219
+ >
220
+ {/* Animated border */}
221
+ <div className="absolute inset-0 rounded-3xl animated-border opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
222
+
223
+ <div className="relative glass-card rounded-3xl p-8 transition-all duration-500 hover:scale-105 hover:-translate-y-2">
224
+ {/* Icon */}
225
+ <stat.icon className="h-6 w-6 text-primary mx-auto mb-3 opacity-60 group-hover:opacity-100 group-hover:scale-110 transition-all duration-300" suppressHydrationWarning />
226
+
227
+ {/* Value with counter animation */}
228
+ <div className="text-4xl md:text-5xl font-bold gradient-text mb-2 transition-all duration-300 group-hover:scale-110 counter-animate">
229
+ {stat.value}
230
+ </div>
231
+
232
+ {/* Label */}
233
+ <p className="text-sm text-muted-foreground transition-colors duration-300 group-hover:text-foreground">
234
+ {stat.label}
235
+ </p>
236
+
237
+ {/* Shine effect on hover */}
238
+ <div className="absolute inset-0 rounded-3xl shine-effect pointer-events-none" />
239
+ </div>
240
+ </div>
241
+ ))}
242
+ </div>
243
+ </div>
244
+
245
+ {/* Scroll indicator */}
246
+ <div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-fade-in animate-delay-7">
247
+ <div className="scroll-indicator cursor-pointer hover:scale-110 transition-transform" onClick={() => {
248
+ window.scrollTo({ top: window.innerHeight, behavior: 'smooth' })
249
+ }} />
250
+ </div>
251
+ </div>
252
+
253
+ {/* CSS for dash animation */}
254
+ <style jsx>{`
255
+ @keyframes dash {
256
+ to {
257
+ stroke-dashoffset: 0;
258
+ }
259
+ }
260
+ `}</style>
261
+ </section>
262
+ )
263
+ }
components/home/ServicesOverview.tsx ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
4
+ import { Code, Palette, Smartphone, LineChart, ArrowUpRight, Sparkles } from "lucide-react"
5
+ import Link from "next/link"
6
+ import { useRef, useState, useEffect } from "react"
7
+
8
+ const services = [
9
+ {
10
+ title: "Web Development",
11
+ description: "Custom websites and web applications built with modern technologies like Next.js, React, and Node.js.",
12
+ icon: Code,
13
+ gradient: "from-blue-500 via-cyan-400 to-teal-400",
14
+ accentColor: "hsl(199, 89%, 48%)",
15
+ features: ["Next.js", "React", "Node.js"],
16
+ },
17
+ {
18
+ title: "Mobile App Development",
19
+ description: "Native and cross-platform mobile apps for iOS and Android using React Native and Flutter.",
20
+ icon: Smartphone,
21
+ gradient: "from-violet-500 via-purple-400 to-fuchsia-400",
22
+ accentColor: "hsl(280, 65%, 60%)",
23
+ features: ["React Native", "Flutter", "iOS & Android"],
24
+ },
25
+ {
26
+ title: "UI/UX Design",
27
+ description: "User-centric design solutions that enhance engagement, usability, and brand identity.",
28
+ icon: Palette,
29
+ gradient: "from-pink-500 via-rose-400 to-orange-400",
30
+ accentColor: "hsl(340, 75%, 55%)",
31
+ features: ["Figma", "Prototyping", "Design Systems"],
32
+ },
33
+ {
34
+ title: "SEO Optimization",
35
+ description: "Data-driven strategies to improve your search engine rankings, visibility, and organic traffic.",
36
+ icon: LineChart,
37
+ gradient: "from-amber-500 via-orange-400 to-red-400",
38
+ accentColor: "hsl(30, 80%, 55%)",
39
+ features: ["Analytics", "Keyword Research", "Technical SEO"],
40
+ },
41
+ ]
42
+
43
+ function ServiceCard({ service, index }: { service: typeof services[0]; index: number }) {
44
+ const cardRef = useRef<HTMLDivElement>(null)
45
+ const [rotateX, setRotateX] = useState(0)
46
+ const [rotateY, setRotateY] = useState(0)
47
+ const [isHovered, setIsHovered] = useState(false)
48
+
49
+ const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
50
+ if (!cardRef.current) return
51
+ const rect = cardRef.current.getBoundingClientRect()
52
+ const centerX = rect.left + rect.width / 2
53
+ const centerY = rect.top + rect.height / 2
54
+ const mouseX = e.clientX - centerX
55
+ const mouseY = e.clientY - centerY
56
+
57
+ setRotateX(-mouseY / 10)
58
+ setRotateY(mouseX / 10)
59
+ }
60
+
61
+ const handleMouseLeave = () => {
62
+ setRotateX(0)
63
+ setRotateY(0)
64
+ setIsHovered(false)
65
+ }
66
+
67
+ return (
68
+ <div
69
+ ref={cardRef}
70
+ className="group relative h-full"
71
+ style={{
72
+ perspective: '1000px',
73
+ animationDelay: `${index * 0.1}s`,
74
+ }}
75
+ >
76
+ <Link href="/services" aria-label={`Learn more about ${service.title}`} className="block h-full">
77
+ <div
78
+ className="relative h-full cursor-pointer transition-all duration-300"
79
+ style={{
80
+ transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
81
+ transformStyle: 'preserve-3d',
82
+ }}
83
+ onMouseMove={handleMouseMove}
84
+ onMouseEnter={() => setIsHovered(true)}
85
+ onMouseLeave={handleMouseLeave}
86
+ >
87
+ {/* Glow effect */}
88
+ <div
89
+ className="absolute -inset-1 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 blur-xl"
90
+ style={{
91
+ background: `linear-gradient(135deg, ${service.accentColor}, transparent)`,
92
+ }}
93
+ />
94
+
95
+ {/* Card */}
96
+ <Card className="relative h-full overflow-hidden glass-card rounded-3xl border-0 transition-all duration-300 group-hover:shadow-2xl">
97
+ {/* Animated gradient border */}
98
+ <div
99
+ className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
100
+ style={{
101
+ background: `linear-gradient(135deg, ${service.accentColor}20, transparent, ${service.accentColor}20)`,
102
+ }}
103
+ />
104
+
105
+ {/* Spotlight effect */}
106
+ <div
107
+ className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
108
+ style={{
109
+ background: `radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), ${service.accentColor}15, transparent 40%)`,
110
+ }}
111
+ />
112
+
113
+ <CardHeader className="relative z-10 pb-4">
114
+ {/* Icon container */}
115
+ <div
116
+ className={`relative w-16 h-16 rounded-2xl flex items-center justify-center mb-5 transition-all duration-500 group-hover:scale-110 group-hover:rotate-3`}
117
+ style={{
118
+ background: `linear-gradient(135deg, ${service.accentColor}20, ${service.accentColor}10)`,
119
+ boxShadow: isHovered ? `0 10px 40px ${service.accentColor}30` : 'none',
120
+ }}
121
+ >
122
+ <service.icon
123
+ className="w-8 h-8 transition-all duration-300"
124
+ style={{ color: service.accentColor }}
125
+ suppressHydrationWarning
126
+ />
127
+
128
+ {/* Sparkle effect on hover */}
129
+ <Sparkles
130
+ className="absolute -top-1 -right-1 w-4 h-4 text-primary opacity-0 group-hover:opacity-100 transition-all duration-300 group-hover:scale-110"
131
+ suppressHydrationWarning
132
+ />
133
+ </div>
134
+
135
+ {/* Title with arrow */}
136
+ <h3 className="text-xl font-bold leading-tight flex items-center gap-2 group-hover:text-primary transition-colors duration-300">
137
+ {service.title}
138
+ <ArrowUpRight
139
+ className="h-4 w-4 opacity-0 -translate-x-2 -translate-y-2 group-hover:opacity-100 group-hover:translate-x-0 group-hover:translate-y-0 transition-all duration-300 text-primary"
140
+ suppressHydrationWarning
141
+ />
142
+ </h3>
143
+ </CardHeader>
144
+
145
+ <CardContent className="relative z-10">
146
+ <p className="text-base text-muted-foreground group-hover:text-foreground transition-colors duration-300 leading-relaxed mb-4">
147
+ {service.description}
148
+ </p>
149
+
150
+ {/* Feature tags */}
151
+ <div className="flex flex-wrap gap-2">
152
+ {service.features.map((feature) => (
153
+ <span
154
+ key={feature}
155
+ className="px-3 py-1 rounded-full text-xs font-medium transition-all duration-300 group-hover:scale-105"
156
+ style={{
157
+ background: `${service.accentColor}15`,
158
+ color: service.accentColor,
159
+ }}
160
+ >
161
+ {feature}
162
+ </span>
163
+ ))}
164
+ </div>
165
+ </CardContent>
166
+
167
+ {/* Bottom gradient line */}
168
+ <div
169
+ className="absolute bottom-0 left-0 right-0 h-1 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
170
+ style={{
171
+ background: `linear-gradient(90deg, transparent, ${service.accentColor}, transparent)`,
172
+ }}
173
+ />
174
+
175
+ {/* Shine effect */}
176
+ <div className="absolute inset-0 shine-effect pointer-events-none rounded-3xl" />
177
+ </Card>
178
+ </div>
179
+ </Link>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ export function ServicesOverview() {
185
+ const [isVisible, setIsVisible] = useState(false)
186
+ const sectionRef = useRef<HTMLElement>(null)
187
+
188
+ useEffect(() => {
189
+ const observer = new IntersectionObserver(
190
+ ([entry]) => {
191
+ if (entry.isIntersecting) {
192
+ setIsVisible(true)
193
+ }
194
+ },
195
+ { threshold: 0.1 }
196
+ )
197
+
198
+ if (sectionRef.current) {
199
+ observer.observe(sectionRef.current)
200
+ }
201
+
202
+ return () => observer.disconnect()
203
+ }, [])
204
+
205
+ return (
206
+ <section ref={sectionRef} className="py-24 bg-background relative overflow-hidden" id="services">
207
+ {/* Background elements */}
208
+ <div className="absolute inset-0 gradient-mesh opacity-20" />
209
+ <div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
210
+ <div className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl" />
211
+
212
+ <div className="container relative z-10">
213
+ {/* Section header with reveal animation */}
214
+ <div className={`text-center mb-16 transition-all duration-1000 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}>
215
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card mb-6">
216
+ <Sparkles className="h-4 w-4 text-primary animate-pulse" suppressHydrationWarning />
217
+ <span className="text-sm font-medium text-primary">What We Do</span>
218
+ </div>
219
+
220
+ <h2 className="text-4xl md:text-5xl mb-6 font-bold tracking-tight">
221
+ Our <span className="gradient-text">Expertise</span>
222
+ </h2>
223
+
224
+ <p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
225
+ We offer a comprehensive range of digital services to help your business{" "}
226
+ <span className="text-primary font-medium">grow</span> and{" "}
227
+ <span className="text-primary font-medium">succeed</span>.
228
+ </p>
229
+ </div>
230
+
231
+ {/* Services grid */}
232
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
233
+ {services.map((service, i) => (
234
+ <div
235
+ key={service.title}
236
+ className={`transition-all duration-700 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}
237
+ style={{ transitionDelay: `${i * 100}ms` }}
238
+ >
239
+ <ServiceCard service={service} index={i} />
240
+ </div>
241
+ ))}
242
+ </div>
243
+
244
+ {/* Bottom CTA */}
245
+ <div className={`text-center mt-12 transition-all duration-1000 delay-500 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}>
246
+ <p className="text-muted-foreground">
247
+ Need something custom?{" "}
248
+ <Link href="/contact" className="text-primary hover:underline underline-offset-4 font-medium interactive-underline">
249
+ Let&apos;s discuss your project
250
+ </Link>
251
+ </p>
252
+ </div>
253
+ </div>
254
+ </section>
255
+ )
256
+ }
components/home/TestimonialsCarousel.tsx ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Card, CardContent, CardHeader, CardFooter } from "@/components/ui/card"
4
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
5
+ import { Star, Quote, ChevronLeft, ChevronRight } from "lucide-react"
6
+ import { useRef, useState, useEffect, useCallback } from "react"
7
+
8
+ type Testimonial = {
9
+ id: string
10
+ clientName: string
11
+ company: string | null
12
+ content: string
13
+ rating: number
14
+ imageUrl: string | null
15
+ }
16
+
17
+ /* Static fallback used when the API is unreachable (build-time, etc.) */
18
+ const fallbackTestimonials: Testimonial[] = [
19
+ {
20
+ id: "fallback-1",
21
+ clientName: "Sarah Johnson",
22
+ company: "TechFlow",
23
+ content:
24
+ "NexaFlow transformed our online presence. Their attention to detail and technical expertise is unmatched. They delivered exactly what we needed.",
25
+ rating: 5,
26
+ imageUrl: null,
27
+ },
28
+ {
29
+ id: "fallback-2",
30
+ clientName: "Michael Chen",
31
+ company: "StartupX",
32
+ content:
33
+ "The team delivered our MVP ahead of schedule and it looks fantastic. They truly understood our vision and brought it to life.",
34
+ rating: 5,
35
+ imageUrl: null,
36
+ },
37
+ {
38
+ id: "fallback-3",
39
+ clientName: "Emily Davis",
40
+ company: "Creative Studio",
41
+ content:
42
+ "Professional, responsive, and incredibly talented. They truly understand modern web design and the latest technologies.",
43
+ rating: 5,
44
+ imageUrl: null,
45
+ },
46
+ {
47
+ id: "fallback-4",
48
+ clientName: "David Wilson",
49
+ company: "InnovateCo",
50
+ content:
51
+ "Working with NexaFlow was a game-changer for our business. The website they built exceeded our expectations in every way.",
52
+ rating: 5,
53
+ imageUrl: null,
54
+ },
55
+ {
56
+ id: "fallback-5",
57
+ clientName: "Lisa Anderson",
58
+ company: "GrowthHub",
59
+ content:
60
+ "From concept to launch, the process was smooth and professional. Our new app has received amazing feedback from users.",
61
+ rating: 5,
62
+ imageUrl: null,
63
+ },
64
+ ]
65
+
66
+ function getInitials(name: string): string {
67
+ return name
68
+ .split(" ")
69
+ .map((w) => w[0])
70
+ .join("")
71
+ .toUpperCase()
72
+ .slice(0, 2)
73
+ }
74
+
75
+ function TestimonialCard({ testimonial, isActive }: { testimonial: Testimonial; isActive: boolean }) {
76
+ return (
77
+ <Card
78
+ className={`relative h-full glass-card border-0 transition-all duration-500 rounded-3xl overflow-hidden ${
79
+ isActive ? "scale-100 opacity-100" : "scale-95 opacity-70"
80
+ }`}
81
+ >
82
+ {/* Animated gradient background */}
83
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
84
+
85
+ {/* Quote decoration */}
86
+ <div className="absolute top-6 right-6 group-hover:scale-110 group-hover:rotate-12 transition-all duration-300" aria-hidden="true">
87
+ <Quote className="h-10 w-10 text-primary/20" suppressHydrationWarning />
88
+ </div>
89
+
90
+ <CardHeader className="pb-2 relative z-10">
91
+ <div className="flex gap-0.5 mb-4" role="img" aria-label={`${testimonial.rating} out of 5 stars`}>
92
+ {[...Array(testimonial.rating)].map((_, j) => (
93
+ <Star
94
+ key={j}
95
+ className="w-5 h-5 fill-amber-400 text-amber-400 group-hover:scale-110 transition-all duration-300"
96
+ style={{ transitionDelay: `${j * 50}ms` }}
97
+ suppressHydrationWarning
98
+ />
99
+ ))}
100
+ </div>
101
+ </CardHeader>
102
+
103
+ <CardContent className="relative z-10">
104
+ <blockquote className="text-base text-foreground leading-relaxed">
105
+ &ldquo;{testimonial.content}&rdquo;
106
+ </blockquote>
107
+ </CardContent>
108
+
109
+ <CardFooter className="flex items-center gap-4 mt-auto pt-4 relative z-10">
110
+ <Avatar className="h-12 w-12 glass-card group-hover:scale-110 transition-all duration-300 ring-2 ring-primary/20">
111
+ <AvatarFallback className="bg-gradient-to-br from-primary/20 to-accent/20 text-primary text-sm font-semibold">
112
+ {getInitials(testimonial.clientName)}
113
+ </AvatarFallback>
114
+ </Avatar>
115
+ <div>
116
+ <p className="font-semibold text-sm group-hover:text-primary transition-colors duration-300">
117
+ {testimonial.clientName}
118
+ </p>
119
+ {testimonial.company && (
120
+ <p className="text-xs text-muted-foreground">{testimonial.company}</p>
121
+ )}
122
+ </div>
123
+ </CardFooter>
124
+
125
+ {/* Bottom accent line */}
126
+ <div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-primary via-accent to-primary transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left" />
127
+
128
+ {/* Shine effect */}
129
+ <div className="absolute inset-0 shine-effect pointer-events-none rounded-3xl" />
130
+ </Card>
131
+ )
132
+ }
133
+
134
+ export function TestimonialsCarousel() {
135
+ const [testimonials, setTestimonials] = useState<Testimonial[]>(fallbackTestimonials)
136
+ const [currentIndex, setCurrentIndex] = useState(0)
137
+ const [isVisible, setIsVisible] = useState(false)
138
+ const [isPaused, setIsPaused] = useState(false)
139
+ const sectionRef = useRef<HTMLElement>(null)
140
+
141
+ // Fetch testimonials
142
+ useEffect(() => {
143
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api"
144
+ fetch(`${apiUrl}/testimonials`)
145
+ .then((res) => res.json())
146
+ .then((data: Testimonial[]) => {
147
+ if (data.length > 0) setTestimonials(data)
148
+ })
149
+ .catch(() => {
150
+ // Use fallback testimonials
151
+ })
152
+ }, [])
153
+
154
+ // Intersection observer for reveal animation
155
+ useEffect(() => {
156
+ const observer = new IntersectionObserver(
157
+ ([entry]) => {
158
+ if (entry.isIntersecting) {
159
+ setIsVisible(true)
160
+ }
161
+ },
162
+ { threshold: 0.1 }
163
+ )
164
+
165
+ if (sectionRef.current) {
166
+ observer.observe(sectionRef.current)
167
+ }
168
+
169
+ return () => observer.disconnect()
170
+ }, [])
171
+
172
+ // Auto-scroll carousel
173
+ useEffect(() => {
174
+ if (isPaused || testimonials.length <= 3) return
175
+
176
+ const interval = setInterval(() => {
177
+ setCurrentIndex((prev) => (prev + 1) % testimonials.length)
178
+ }, 5000)
179
+
180
+ return () => clearInterval(interval)
181
+ }, [isPaused, testimonials.length])
182
+
183
+ const goToPrevious = useCallback(() => {
184
+ setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length)
185
+ }, [testimonials.length])
186
+
187
+ const goToNext = useCallback(() => {
188
+ setCurrentIndex((prev) => (prev + 1) % testimonials.length)
189
+ }, [testimonials.length])
190
+
191
+ const goToSlide = useCallback((index: number) => {
192
+ setCurrentIndex(index)
193
+ }, [])
194
+
195
+ // Get visible testimonials for carousel
196
+ const getVisibleTestimonials = () => {
197
+ const visible = []
198
+ for (let i = 0; i < 3; i++) {
199
+ const index = (currentIndex + i) % testimonials.length
200
+ visible.push({ testimonial: testimonials[index], position: i })
201
+ }
202
+ return visible
203
+ }
204
+
205
+ return (
206
+ <section
207
+ ref={sectionRef}
208
+ className="py-24 bg-background relative overflow-hidden"
209
+ id="testimonials"
210
+ aria-label="Client testimonials"
211
+ onMouseEnter={() => setIsPaused(true)}
212
+ onMouseLeave={() => setIsPaused(false)}
213
+ >
214
+ {/* Background decorations */}
215
+ <div className="absolute inset-0 gradient-mesh opacity-20" />
216
+ <div className="absolute top-1/4 right-0 w-96 h-96 bg-primary/10 rounded-full blur-3xl" />
217
+ <div className="absolute bottom-1/4 left-0 w-96 h-96 bg-accent/10 rounded-full blur-3xl" />
218
+
219
+ {/* Floating quote icons */}
220
+ <div className="absolute inset-0 overflow-hidden pointer-events-none opacity-5">
221
+ {[...Array(6)].map((_, i) => (
222
+ <Quote
223
+ key={i}
224
+ className="absolute w-16 h-16 text-primary animate-float"
225
+ style={{
226
+ top: `${20 + i * 15}%`,
227
+ left: `${10 + i * 15}%`,
228
+ animationDelay: `${i * 0.5}s`,
229
+ }}
230
+ suppressHydrationWarning
231
+ />
232
+ ))}
233
+ </div>
234
+
235
+ <div className="container relative z-10">
236
+ {/* Section header */}
237
+ <div
238
+ className={`text-center mb-14 transition-all duration-1000 ${
239
+ isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
240
+ }`}
241
+ >
242
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card mb-6">
243
+ <Star className="w-4 h-4 text-amber-400 fill-amber-400" suppressHydrationWarning />
244
+ <span className="text-sm font-medium text-primary">Testimonials</span>
245
+ </div>
246
+
247
+ <h2 className="text-4xl md:text-5xl font-bold tracking-tight">
248
+ What Our <span className="gradient-text">Clients</span> Say
249
+ </h2>
250
+
251
+ <p className="text-lg text-muted-foreground max-w-2xl mx-auto mt-4">
252
+ Don&apos;t just take our word for it — hear from the businesses we&apos;ve helped succeed.
253
+ </p>
254
+ </div>
255
+
256
+ {/* Carousel */}
257
+ <div
258
+ className={`relative transition-all duration-1000 delay-200 ${
259
+ isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
260
+ }`}
261
+ >
262
+ {/* Navigation buttons */}
263
+ <button
264
+ onClick={goToPrevious}
265
+ className="absolute left-0 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full glass-card flex items-center justify-center hover:bg-primary/20 transition-all duration-300 hover:scale-110 group"
266
+ aria-label="Previous testimonial"
267
+ >
268
+ <ChevronLeft className="w-6 h-6 text-primary group-hover:-translate-x-0.5 transition-transform" suppressHydrationWarning />
269
+ </button>
270
+
271
+ <button
272
+ onClick={goToNext}
273
+ className="absolute right-0 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full glass-card flex items-center justify-center hover:bg-primary/20 transition-all duration-300 hover:scale-110 group"
274
+ aria-label="Next testimonial"
275
+ >
276
+ <ChevronRight className="w-6 h-6 text-primary group-hover:translate-x-0.5 transition-transform" suppressHydrationWarning />
277
+ </button>
278
+
279
+ {/* Cards container */}
280
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 px-8 md:px-16">
281
+ {getVisibleTestimonials().map(({ testimonial, position }) => (
282
+ <div
283
+ key={`${testimonial.id}-${currentIndex}-${position}`}
284
+ className="group"
285
+ style={{
286
+ animationDelay: `${position * 100}ms`,
287
+ }}
288
+ >
289
+ <TestimonialCard testimonial={testimonial} isActive={position === 1} />
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </div>
294
+
295
+ {/* Dots navigation */}
296
+ <div
297
+ className={`flex justify-center gap-2 mt-8 transition-all duration-1000 delay-300 ${
298
+ isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
299
+ }`}
300
+ >
301
+ {testimonials.slice(0, 6).map((_, index) => (
302
+ <button
303
+ key={index}
304
+ onClick={() => goToSlide(index)}
305
+ className={`w-2 h-2 rounded-full transition-all duration-300 ${
306
+ index === currentIndex
307
+ ? "w-8 bg-primary"
308
+ : "bg-muted-foreground/30 hover:bg-muted-foreground/50"
309
+ }`}
310
+ aria-label={`Go to testimonial ${index + 1}`}
311
+ />
312
+ ))}
313
+ </div>
314
+
315
+ {/* Trust indicator */}
316
+ <div
317
+ className={`text-center mt-8 transition-all duration-1000 delay-400 ${
318
+ isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
319
+ }`}
320
+ >
321
+ <p className="text-sm text-muted-foreground">
322
+ Trusted by <span className="text-primary font-semibold">50+</span> satisfied clients
323
+ </p>
324
+ </div>
325
+ </div>
326
+ </section>
327
+ )
328
+ }
components/home/TrustIndicators.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { Building2, Sparkles, Star, TrendingUp, Users, Award } from "lucide-react"
4
+ import { useRef, useState, useEffect } from "react"
5
+
6
+ const companies = [
7
+ { name: "Acme Corp", icon: Building2 },
8
+ { name: "Global Tech", icon: TrendingUp },
9
+ { name: "Nebula Inc", icon: Sparkles },
10
+ { name: "CodeFlow", icon: Award },
11
+ { name: "FutureSoft", icon: Users },
12
+ ]
13
+
14
+ const stats = [
15
+ { value: "50+", label: "Projects Delivered", icon: Star },
16
+ { value: "98%", label: "Client Satisfaction", icon: TrendingUp },
17
+ { value: "30+", label: "Happy Clients", icon: Users },
18
+ ]
19
+
20
+ export function TrustIndicators() {
21
+ const sectionRef = useRef<HTMLElement>(null)
22
+ const [isVisible, setIsVisible] = useState(false)
23
+
24
+ useEffect(() => {
25
+ const observer = new IntersectionObserver(
26
+ ([entry]) => {
27
+ if (entry.isIntersecting) {
28
+ setIsVisible(true)
29
+ }
30
+ },
31
+ { threshold: 0.2 }
32
+ )
33
+
34
+ if (sectionRef.current) {
35
+ observer.observe(sectionRef.current)
36
+ }
37
+
38
+ return () => observer.disconnect()
39
+ }, [])
40
+
41
+ return (
42
+ <section ref={sectionRef} className="py-14 bg-background relative overflow-hidden" aria-label="Trusted by">
43
+ {/* Background elements */}
44
+ <div className="absolute inset-0 gradient-mesh opacity-10" />
45
+ <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[200px] bg-primary/5 rounded-full blur-3xl" />
46
+
47
+ <div className="container relative z-10">
48
+ {/* Header */}
49
+ <div
50
+ className={`text-center mb-12 transition-all duration-700 ${
51
+ isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
52
+ }`}
53
+ >
54
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-card">
55
+ <Sparkles className="w-4 h-4 text-primary animate-pulse" suppressHydrationWarning />
56
+ <span className="text-sm font-medium text-muted-foreground">Trusted by innovative companies</span>
57
+ </div>
58
+ </div>
59
+
60
+ {/* Animated company logos - Infinite scroll effect */}
61
+ <div className="relative overflow-hidden mb-12">
62
+ {/* Gradient masks */}
63
+ <div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-background to-transparent z-10 pointer-events-none" />
64
+ <div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-background to-transparent z-10 pointer-events-none" />
65
+
66
+ {/* Scrolling container */}
67
+ <div className="flex gap-8 animate-scroll">
68
+ {[...companies, ...companies].map((company, i) => (
69
+ <div
70
+ key={`${company.name}-${i}`}
71
+ className="group flex items-center gap-3 text-muted-foreground hover:text-primary transition-all duration-300 cursor-default glass-card px-6 py-3 rounded-2xl shrink-0 hover:scale-105 hover:shadow-lg"
72
+ >
73
+ <company.icon
74
+ className="h-5 w-5 group-hover:scale-110 group-hover:rotate-12 transition-transform duration-300"
75
+ suppressHydrationWarning
76
+ />
77
+ <span className="text-lg font-bold tracking-tight">{company.name}</span>
78
+ </div>
79
+ ))}
80
+ </div>
81
+ </div>
82
+
83
+ {/* Stats row */}
84
+ <div
85
+ className={`flex flex-wrap items-center justify-center gap-8 md:gap-16 transition-all duration-700 delay-200 ${
86
+ isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
87
+ }`}
88
+ >
89
+ {stats.map((stat, i) => (
90
+ <div
91
+ key={stat.label}
92
+ className="group text-center cursor-default"
93
+ style={{ transitionDelay: `${i * 100}ms` }}
94
+ >
95
+ <div className="flex items-center gap-2 mb-1">
96
+ <stat.icon className="w-4 h-4 text-primary opacity-50 group-hover:opacity-100 group-hover:scale-110 transition-all duration-300" suppressHydrationWarning />
97
+ <span className="text-2xl md:text-3xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300 inline-block">
98
+ {stat.value}
99
+ </span>
100
+ </div>
101
+ <span className="text-xs md:text-sm text-muted-foreground group-hover:text-foreground transition-colors duration-300">
102
+ {stat.label}
103
+ </span>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+
109
+ {/* CSS for scroll animation */}
110
+ <style jsx>{`
111
+ @keyframes scroll {
112
+ 0% {
113
+ transform: translateX(0);
114
+ }
115
+ 100% {
116
+ transform: translateX(-50%);
117
+ }
118
+ }
119
+ .animate-scroll {
120
+ animation: scroll 30s linear infinite;
121
+ }
122
+ .animate-scroll:hover {
123
+ animation-play-state: paused;
124
+ }
125
+ `}</style>
126
+ </section>
127
+ )
128
+ }
components/layout/Footer.tsx ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { Github, Linkedin, Mail, ArrowRight, Sparkles, Heart } from "lucide-react"
7
+ import { useRef, useState, useEffect } from "react"
8
+
9
+ const footerLinks = [
10
+ {
11
+ title: "Services",
12
+ links: [
13
+ { name: "Web Development", href: "/services" },
14
+ { name: "App Development", href: "/services" },
15
+ { name: "UI/UX Design", href: "/services" },
16
+ { name: "SEO Optimization", href: "/services" },
17
+ ],
18
+ },
19
+ {
20
+ title: "Company",
21
+ links: [
22
+ { name: "About Us", href: "/about" },
23
+ { name: "Portfolio", href: "/portfolio" },
24
+ { name: "Blog", href: "/blog" },
25
+ { name: "Contact", href: "/contact" },
26
+ ],
27
+ },
28
+ {
29
+ title: "Legal",
30
+ links: [
31
+ { name: "Privacy Policy", href: "/privacy" },
32
+ { name: "Terms of Service", href: "/terms" },
33
+ { name: "Cookie Policy", href: "/cookies" },
34
+ ],
35
+ },
36
+ ]
37
+
38
+ const socialLinks = [
39
+ { icon: Github, href: "#", label: "GitHub", color: "hover:text-white" },
40
+ { icon: Linkedin, href: "#", label: "LinkedIn", color: "hover:text-blue-400" },
41
+ { icon: Mail, href: "mailto:hello@nexaflow.dev", label: "Email", color: "hover:text-primary" },
42
+ ]
43
+
44
+ export function Footer() {
45
+ const [email, setEmail] = useState("")
46
+ const [isSubscribed, setIsSubscribed] = useState(false)
47
+ const sectionRef = useRef<HTMLElement>(null)
48
+ const [isVisible, setIsVisible] = useState(false)
49
+
50
+ useEffect(() => {
51
+ const observer = new IntersectionObserver(
52
+ ([entry]) => {
53
+ if (entry.isIntersecting) {
54
+ setIsVisible(true)
55
+ }
56
+ },
57
+ { threshold: 0.1 }
58
+ )
59
+
60
+ if (sectionRef.current) {
61
+ observer.observe(sectionRef.current)
62
+ }
63
+
64
+ return () => observer.disconnect()
65
+ }, [])
66
+
67
+ const handleSubscribe = (e: React.FormEvent) => {
68
+ e.preventDefault()
69
+ if (email) {
70
+ setIsSubscribed(true)
71
+ setEmail("")
72
+ setTimeout(() => setIsSubscribed(false), 3000)
73
+ }
74
+ }
75
+
76
+ return (
77
+ <footer ref={sectionRef} className="w-full bg-muted/20 relative overflow-hidden">
78
+ {/* Background decorations */}
79
+ <div className="absolute inset-0 gradient-mesh opacity-10" />
80
+ <div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl animate-float" style={{ animationDelay: '0s' }} />
81
+ <div className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/10 rounded-full blur-3xl animate-float" style={{ animationDelay: '5s' }} />
82
+
83
+ {/* Animated grid */}
84
+ <div className="absolute inset-0 opacity-5">
85
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,hsl(var(--primary))_1px,transparent_1px)] bg-size-[32px_32px]" />
86
+ </div>
87
+
88
+ <div className={`container py-14 md:py-16 lg:py-20 relative z-10 transition-all duration-1000 ${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'}`}>
89
+ <div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-5">
90
+ {/* Brand column */}
91
+ <div className="flex flex-col gap-4 lg:col-span-2 lg:pr-8">
92
+ <Link href="/" className="text-xl font-bold tracking-tight group inline-flex items-center gap-2">
93
+ <span className="group-hover:scale-105 transition-transform inline-block">
94
+ Nexa<span className="gradient-text">Flow</span>
95
+ </span>
96
+ <Sparkles className="w-4 h-4 text-primary opacity-0 group-hover:opacity-100 group-hover:rotate-12 transition-all duration-300" suppressHydrationWarning />
97
+ </Link>
98
+ <p className="text-sm text-muted-foreground leading-relaxed max-w-xs">
99
+ Building digital experiences that matter. We partner with forward-thinking companies to create impactful digital products.
100
+ </p>
101
+
102
+ {/* Social icons */}
103
+ <div className="flex gap-3 mt-2">
104
+ {socialLinks.map((social) => (
105
+ <Link
106
+ key={social.label}
107
+ href={social.href}
108
+ className={`flex h-10 w-10 items-center justify-center rounded-xl glass-card text-muted-foreground hover:scale-110 transition-all duration-300 group ${social.color}`}
109
+ aria-label={social.label}
110
+ >
111
+ <social.icon className="h-4 w-4 transition-transform duration-300 group-hover:scale-110" suppressHydrationWarning />
112
+ </Link>
113
+ ))}
114
+ </div>
115
+ </div>
116
+
117
+ {/* Link columns */}
118
+ {footerLinks.map((section, i) => (
119
+ <div
120
+ key={section.title}
121
+ className="flex flex-col gap-4"
122
+ style={{ transitionDelay: `${i * 100}ms` }}
123
+ >
124
+ <h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
125
+ <span className="w-1.5 h-1.5 rounded-full bg-primary" />
126
+ {section.title}
127
+ </h3>
128
+ <ul className="flex flex-col gap-3">
129
+ {section.links.map((link) => (
130
+ <li key={link.name}>
131
+ <Link
132
+ href={link.href}
133
+ className="text-sm text-muted-foreground hover:text-primary transition-colors duration-200 interactive-underline inline-block"
134
+ >
135
+ {link.name}
136
+ </Link>
137
+ </li>
138
+ ))}
139
+ </ul>
140
+ </div>
141
+ ))}
142
+ </div>
143
+
144
+ {/* Newsletter signup */}
145
+ <div className="mt-12 relative">
146
+ {/* Animated border */}
147
+ <div className="absolute -inset-0.5 rounded-3xl animated-border opacity-30" />
148
+
149
+ <div className="relative glass-card rounded-3xl p-6 md:p-8 overflow-hidden">
150
+ {/* Inner glow */}
151
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5" />
152
+
153
+ <div className="relative z-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
154
+ <div>
155
+ <h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
156
+ <Mail className="w-5 h-5 text-primary" suppressHydrationWarning />
157
+ Stay in the loop
158
+ </h3>
159
+ <p className="text-sm text-muted-foreground">Get the latest insights on web development and design delivered to your inbox.</p>
160
+ </div>
161
+ <form className="flex w-full md:w-auto gap-3" onSubmit={handleSubscribe}>
162
+ <Input
163
+ type="email"
164
+ placeholder="you@example.com"
165
+ value={email}
166
+ onChange={(e) => setEmail(e.target.value)}
167
+ className="w-full md:w-64 glass-card border-0 focus:ring-2 focus:ring-primary/50"
168
+ />
169
+ <Button type="submit" className="shine-effect group">
170
+ {isSubscribed ? (
171
+ <span className="flex items-center gap-2">
172
+ <Sparkles className="w-4 h-4" suppressHydrationWarning />
173
+ Subscribed!
174
+ </span>
175
+ ) : (
176
+ <span className="flex items-center">
177
+ Subscribe
178
+ <ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform" suppressHydrationWarning />
179
+ </span>
180
+ )}
181
+ </Button>
182
+ </form>
183
+ </div>
184
+
185
+ {/* Shine effect */}
186
+ <div className="absolute inset-0 shine-effect pointer-events-none rounded-3xl" />
187
+ </div>
188
+ </div>
189
+
190
+ {/* Bottom bar */}
191
+ <div className="mt-14 border-t border-border/50 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
192
+ <p className="text-sm text-muted-foreground">
193
+ &copy; {new Date().getFullYear()} NexaFlow. All rights reserved.
194
+ </p>
195
+ <p className="text-sm text-muted-foreground flex items-center gap-1">
196
+ Designed with{" "}
197
+ <Heart className="w-4 h-4 text-red-500 fill-red-500 animate-pulse" suppressHydrationWarning />
198
+ {" "}by{" "}
199
+ <span className="text-primary font-medium">NexaFlow</span>
200
+ </p>
201
+ </div>
202
+ </div>
203
+ </footer>
204
+ )
205
+ }
components/layout/Header.tsx ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import Link from "next/link"
5
+ import { usePathname } from "next/navigation"
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
9
+ import { Menu, Sparkles } from "lucide-react"
10
+ import { useSession, signOut } from "next-auth/react"
11
+ import { ModeToggle } from "@/components/mode-toggle"
12
+
13
+ const navItems = [
14
+ { name: "Services", href: "/services" },
15
+ { name: "Portfolio", href: "/portfolio" },
16
+ { name: "Blog", href: "/blog" },
17
+ { name: "About", href: "/about" },
18
+ { name: "Contact", href: "/contact" },
19
+ ]
20
+
21
+ function AuthButtons({ mobile, onAction }: { mobile?: boolean; onAction?: () => void }) {
22
+ const { data: session } = useSession()
23
+
24
+ if (session) {
25
+ return (
26
+ <div className={cn("flex gap-2", mobile ? "flex-col" : "items-center")}>
27
+ <span className="text-sm text-muted-foreground truncate max-w-[140px]">
28
+ {session.user?.email}
29
+ </span>
30
+ <Button
31
+ variant="outline"
32
+ size="sm"
33
+ className="glass-card hover:bg-primary/10"
34
+ onClick={() => {
35
+ signOut({ callbackUrl: "/admin/login" })
36
+ onAction?.()
37
+ }}
38
+ >
39
+ Logout
40
+ </Button>
41
+ <Button asChild variant="default" size="sm" onClick={onAction} className="shine-effect">
42
+ <Link href="/admin/dashboard">Dashboard</Link>
43
+ </Button>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ return (
49
+ <div className={cn("flex gap-2", mobile ? "flex-col" : "items-center")}>
50
+ <Button asChild size="sm" onClick={onAction} className="shine-effect">
51
+ <Link href="/quote">Get a Quote</Link>
52
+ </Button>
53
+ <Button asChild variant="ghost" size="sm" onClick={onAction} className="hover:bg-primary/10">
54
+ <Link href="/admin/login">Login</Link>
55
+ </Button>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ export function Header() {
61
+ const pathname = usePathname()
62
+ const [isOpen, setIsOpen] = React.useState(false)
63
+ const [scrolled, setScrolled] = React.useState(false)
64
+
65
+ React.useEffect(() => {
66
+ const handleScroll = () => {
67
+ setScrolled(window.scrollY > 20)
68
+ }
69
+
70
+ window.addEventListener('scroll', handleScroll)
71
+ return () => window.removeEventListener('scroll', handleScroll)
72
+ }, [])
73
+
74
+ return (
75
+ <header
76
+ className={cn(
77
+ "sticky top-0 z-50 w-full transition-all duration-500 animate-fade-in-down",
78
+ scrolled
79
+ ? "glass-card border-b border-border/20 shadow-lg"
80
+ : "bg-transparent"
81
+ )}
82
+ >
83
+ <div className="container flex h-16 items-center justify-between">
84
+ {/* Logo */}
85
+ <Link href="/" className="flex items-center gap-2 group">
86
+ <div className="relative">
87
+ <span className="text-xl font-bold tracking-tight transition-all duration-300 group-hover:scale-105 inline-block">
88
+ Nexa<span className="gradient-text">Flow</span>
89
+ </span>
90
+ {/* Animated underline on hover */}
91
+ <div className="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-primary to-accent group-hover:w-full transition-all duration-300 rounded-full" />
92
+ </div>
93
+ <Sparkles className="w-4 h-4 text-primary opacity-0 group-hover:opacity-100 group-hover:rotate-12 transition-all duration-300" suppressHydrationWarning />
94
+ </Link>
95
+
96
+ {/* Desktop Navigation */}
97
+ <nav className="hidden md:flex items-center gap-1">
98
+ {navItems.map((item) => {
99
+ const isActive = pathname === item.href
100
+ return (
101
+ <Link
102
+ key={item.href}
103
+ href={item.href}
104
+ className={cn(
105
+ "relative px-5 py-2.5 text-sm font-semibold rounded-2xl transition-all duration-300 group",
106
+ isActive
107
+ ? "text-primary glass-card"
108
+ : "text-muted-foreground hover:text-primary hover:bg-primary/5"
109
+ )}
110
+ >
111
+ <span className="relative z-10">{item.name}</span>
112
+ {/* Active indicator */}
113
+ {isActive && (
114
+ <div className="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary animate-pulse" />
115
+ )}
116
+ {/* Hover effect */}
117
+ <div className={cn(
118
+ "absolute inset-0 rounded-2xl bg-primary/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300",
119
+ isActive && "opacity-100"
120
+ )} />
121
+ </Link>
122
+ )
123
+ })}
124
+ </nav>
125
+
126
+ {/* Desktop Actions */}
127
+ <div className="flex items-center gap-3">
128
+ <div className="hidden md:flex items-center gap-2">
129
+ <ModeToggle />
130
+ <AuthButtons />
131
+ </div>
132
+
133
+ {/* Mobile Menu */}
134
+ <div className="md:hidden flex items-center gap-2">
135
+ <ModeToggle />
136
+ <Sheet open={isOpen} onOpenChange={setIsOpen}>
137
+ <SheetTrigger asChild>
138
+ <Button variant="ghost" size="icon" className="glass-card hover:bg-primary/10">
139
+ <Menu className="h-5 w-5" suppressHydrationWarning />
140
+ <span className="sr-only">Toggle menu</span>
141
+ </Button>
142
+ </SheetTrigger>
143
+ <SheetContent side="right" className="w-80 max-w-[80vw] glass-card border-l border-border/20 p-0">
144
+ <div className="flex flex-col gap-6 p-6">
145
+ <Link
146
+ href="/"
147
+ onClick={() => setIsOpen(false)}
148
+ className="text-xl font-bold hover:text-primary transition-colors flex items-center gap-2 group"
149
+ >
150
+ Nexa<span className="gradient-text">Flow</span>
151
+ <Sparkles className="w-4 h-4 text-primary opacity-0 group-hover:opacity-100 transition-opacity" suppressHydrationWarning />
152
+ </Link>
153
+ <nav className="flex flex-col gap-2">
154
+ {navItems.map((item, i) => {
155
+ const isActive = pathname === item.href
156
+ return (
157
+ <Link
158
+ key={item.href}
159
+ href={item.href}
160
+ onClick={() => setIsOpen(false)}
161
+ className={cn(
162
+ "px-4 py-3 text-base font-medium rounded-xl transition-all duration-300 animate-fade-in-up",
163
+ isActive
164
+ ? "text-primary glass-card"
165
+ : "text-muted-foreground hover:text-primary hover:bg-primary/5"
166
+ )}
167
+ style={{ animationDelay: `${i * 0.05}s` }}
168
+ >
169
+ {item.name}
170
+ </Link>
171
+ )
172
+ })}
173
+ </nav>
174
+ <div className="pt-4 border-t border-border/50 animate-fade-in-up animate-delay-3">
175
+ <AuthButtons mobile onAction={() => setIsOpen(false)} />
176
+ </div>
177
+ </div>
178
+ </SheetContent>
179
+ </Sheet>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </header>
184
+ )
185
+ }
components/mode-toggle.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Moon, Sun } from "lucide-react"
5
+ import { useTheme } from "next-themes"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+
15
+ export function ModeToggle() {
16
+ const { setTheme } = useTheme()
17
+
18
+ return (
19
+ <DropdownMenu>
20
+ <DropdownMenuTrigger asChild>
21
+ <Button variant="outline" size="icon">
22
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" suppressHydrationWarning />
23
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" suppressHydrationWarning />
24
+ <span className="sr-only">Toggle theme</span>
25
+ </Button>
26
+ </DropdownMenuTrigger>
27
+ <DropdownMenuContent align="end">
28
+ <DropdownMenuItem onClick={() => setTheme("light")}>
29
+ Light
30
+ </DropdownMenuItem>
31
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
32
+ Dark
33
+ </DropdownMenuItem>
34
+ <DropdownMenuItem onClick={() => setTheme("system")}>
35
+ System
36
+ </DropdownMenuItem>
37
+ </DropdownMenuContent>
38
+ </DropdownMenu>
39
+ )
40
+ }
components/portfolio/ProjectGallery.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import Link from "next/link"
5
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
6
+ import { Button } from "@/components/ui/button"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ export type Project = {
10
+ title: string
11
+ slug: string
12
+ category: string
13
+ description: string
14
+ tags: string[]
15
+ gradient?: string
16
+ emoji?: string
17
+ imageUrl?: string | null
18
+ }
19
+
20
+ const categories = ["All", "Web", "Mobile", "Design", "SEO"]
21
+
22
+ export function ProjectGallery({ projects }: { projects: Project[] }) {
23
+ const [active, setActive] = useState("All")
24
+
25
+ const filtered = active === "All"
26
+ ? projects
27
+ : projects.filter((p) => p.category === active)
28
+
29
+ return (
30
+ <div>
31
+ {/* Filter tabs */}
32
+ <div className="animate-fade-in-up flex flex-wrap justify-center gap-2 mb-12">
33
+ {categories.map((cat) => (
34
+ <Button
35
+ key={cat}
36
+ variant={active === cat ? "default" : "outline"}
37
+ size="sm"
38
+ onClick={() => setActive(cat)}
39
+ className="rounded-full"
40
+ >
41
+ {cat}
42
+ </Button>
43
+ ))}
44
+ </div>
45
+
46
+ {/* Project grid */}
47
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
48
+ {filtered.map((project, i) => (
49
+ <Link key={project.slug} href={`/portfolio/${project.slug}`}>
50
+ <article
51
+ className="animate-fade-in-up h-full"
52
+ style={{ animationDelay: `${i * 0.06}s` }}
53
+ >
54
+ <Card className="group h-full overflow-hidden border hover:border-primary/20 hover:shadow-xl hover:shadow-primary/5 transition-all duration-500 cursor-pointer">
55
+ {/* Thumbnail */}
56
+ <div className="relative aspect-[16/10] bg-muted overflow-hidden flex items-center justify-center">
57
+ {project.imageUrl ? (
58
+ <img
59
+ src={project.imageUrl.startsWith('http') || project.imageUrl.startsWith('/') ? project.imageUrl : `/${project.imageUrl}`}
60
+ alt={project.title}
61
+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
62
+ />
63
+ ) : (
64
+ <div className={`w-full h-full bg-gradient-to-br ${project.gradient || 'from-gray-800 to-gray-600'} flex items-center justify-center`}>
65
+ <span
66
+ className="text-6xl group-hover:scale-110 transition-transform duration-500"
67
+ role="img"
68
+ aria-label={project.title}
69
+ >
70
+ {project.emoji || '🚀'}
71
+ </span>
72
+ </div>
73
+ )}
74
+ </div>
75
+ <CardHeader>
76
+ <p className="text-overline text-primary mb-2">{project.category}</p>
77
+ <h3 className="text-xl font-semibold leading-none">{project.title}</h3>
78
+ </CardHeader>
79
+ <CardContent>
80
+ <p className="text-caption leading-relaxed mb-4">{project.description}</p>
81
+ {/* Technology tags (SF-015) */}
82
+ <div className="flex flex-wrap gap-2">
83
+ {project.tags.map((tag) => (
84
+ <span
85
+ key={tag}
86
+ className="bg-secondary text-secondary-foreground px-3 py-1 rounded-full text-xs font-medium"
87
+ >
88
+ {tag}
89
+ </span>
90
+ ))}
91
+ </div>
92
+ </CardContent>
93
+ </Card>
94
+ </article>
95
+ </Link>
96
+ ))}
97
+ </div>
98
+
99
+ {filtered.length === 0 && (
100
+ <p className="text-center text-muted-foreground py-12">
101
+ No projects in this category yet. Check back soon!
102
+ </p>
103
+ )}
104
+ </div>
105
+ )
106
+ }
components/services/AddOnServices.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link"
2
+ import { Button } from "@/components/ui/button"
3
+ import { Shield, BarChart3, Wrench, Globe, ArrowRight } from "lucide-react"
4
+
5
+ const addons = [
6
+ {
7
+ title: "Maintenance Plan",
8
+ description: "Monthly security updates, performance monitoring, and content changes.",
9
+ price: "From $299/mo",
10
+ icon: Wrench,
11
+ },
12
+ {
13
+ title: "SEO Audit & Strategy",
14
+ description: "Comprehensive audit, keyword research, and a 3-month optimization roadmap.",
15
+ price: "From $999",
16
+ icon: BarChart3,
17
+ },
18
+ {
19
+ title: "Security Hardening",
20
+ description: "Advanced security audits, penetration testing, and compliance setup.",
21
+ price: "From $1,499",
22
+ icon: Shield,
23
+ },
24
+ {
25
+ title: "Analytics & Tracking",
26
+ description: "Google Analytics, heatmaps, conversion tracking, and monthly reports.",
27
+ price: "From $499",
28
+ icon: Globe,
29
+ },
30
+ ]
31
+
32
+ export function AddOnServices() {
33
+ return (
34
+ <section className="py-24 bg-background" id="addons">
35
+ <div className="container">
36
+ <div className="animate-fade-in-up text-center mb-16">
37
+ <p className="text-overline text-primary mb-3">Extras</p>
38
+ <h2 className="sm:text-4xl mb-4">Add-on Services</h2>
39
+ <p className="text-body text-muted-foreground max-w-2xl mx-auto">
40
+ Enhance your project with these supplementary services.
41
+ </p>
42
+ </div>
43
+
44
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
45
+ {addons.map((addon, i) => (
46
+ <div
47
+ key={addon.title}
48
+ className="animate-fade-in-up group rounded-2xl border bg-card p-6 hover:border-primary/20 hover:shadow-lg transition-all duration-500"
49
+ style={{ animationDelay: `${i * 0.1}s` }}
50
+ >
51
+ <div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-4 group-hover:bg-primary group-hover:text-primary-foreground transition-colors duration-300">
52
+ <addon.icon className="w-6 h-6" suppressHydrationWarning />
53
+ </div>
54
+ <h3 className="font-bold mb-2">{addon.title}</h3>
55
+ <p className="text-caption leading-relaxed mb-4">{addon.description}</p>
56
+ <p className="text-sm font-semibold text-primary mb-4">{addon.price}</p>
57
+ <Button asChild variant="outline" size="sm" className="w-full group/btn">
58
+ <Link href="/quote">
59
+ Add to Quote
60
+ <ArrowRight className="ml-2 h-3 w-3 transition-transform group-hover/btn:translate-x-1" suppressHydrationWarning />
61
+ </Link>
62
+ </Button>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ </section>
68
+ )
69
+ }
components/services/FAQSection.tsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ } from "@/components/ui/accordion"
9
+
10
+ const faqCategories = [
11
+ {
12
+ category: "Pricing & Packages",
13
+ questions: [
14
+ {
15
+ q: "What's included in each package?",
16
+ a: "Each package includes a dedicated project manager, custom design, development, quality assurance, and a warranty period. The Starter package covers up to 5 pages, Professional up to 15 pages with CMS, and Enterprise offers unlimited scope with custom integrations.",
17
+ },
18
+ {
19
+ q: "Are there any hidden fees?",
20
+ a: "No. We provide a detailed project quote upfront and any scope changes are discussed and approved before billing. The only additional costs would be third-party services like hosting, domain names, or premium API subscriptions.",
21
+ },
22
+ {
23
+ q: "Do you offer payment plans?",
24
+ a: "Yes! We typically split payments into milestones: 30% upfront, 40% at design approval, and 30% at launch. For Enterprise projects, we can arrange custom payment schedules.",
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ category: "Process & Timeline",
30
+ questions: [
31
+ {
32
+ q: "How long does a typical project take?",
33
+ a: "Starter projects typically take 3-4 weeks, Professional projects 6-8 weeks, and Enterprise projects 10-16+ weeks depending on complexity. We'll provide a detailed timeline during the discovery phase.",
34
+ },
35
+ {
36
+ q: "What does the development process look like?",
37
+ a: "Our process follows four phases: Discovery (requirements & strategy), Design (wireframes & mockups), Development (coding & integration), and Deployment (testing, launch & handoff). You'll have visibility and input at every stage.",
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ category: "Technical",
43
+ questions: [
44
+ {
45
+ q: "What technologies do you use?",
46
+ a: "We primarily build with Next.js, React, TypeScript, and Tailwind CSS for frontend; Node.js with Express or Next.js API routes for backend; and PostgreSQL or MongoDB for databases. For mobile, we use React Native or Flutter.",
47
+ },
48
+ {
49
+ q: "Will my website be SEO-friendly?",
50
+ a: "Absolutely. We build with SEO best practices from day one — semantic HTML, server-side rendering, optimized images, structured data, meta tags, sitemap generation, and fast page load times.",
51
+ },
52
+ {
53
+ q: "Can I update content myself after launch?",
54
+ a: "Yes! We integrate a content management system (CMS) so you can update text, images, blog posts, and more without any coding knowledge.",
55
+ },
56
+ ],
57
+ },
58
+ {
59
+ category: "Support",
60
+ questions: [
61
+ {
62
+ q: "Do you offer ongoing support after launch?",
63
+ a: "Yes. All packages include a 30-day warranty post-launch. We also offer monthly maintenance plans that include security updates, performance monitoring, content updates, and priority support.",
64
+ },
65
+ {
66
+ q: "What if I need changes after the project is delivered?",
67
+ a: "Minor adjustments within the warranty period are covered at no extra cost. For larger changes or new features, we'll provide a quote and timeline. Many of our clients retain us on a monthly basis for ongoing improvements.",
68
+ },
69
+ ],
70
+ },
71
+ ]
72
+
73
+ export function FAQSection() {
74
+ return (
75
+ <section className="py-24 bg-muted/20" id="faq">
76
+ <div className="container max-w-3xl">
77
+ <div className="animate-fade-in-up text-center mb-16">
78
+ <p className="text-overline text-primary mb-3">FAQ</p>
79
+ <h2 className="sm:text-4xl mb-4">Frequently Asked Questions</h2>
80
+ <p className="text-body text-muted-foreground">
81
+ Got questions? We&apos;ve got answers.
82
+ </p>
83
+ </div>
84
+
85
+ <div className="space-y-8">
86
+ {faqCategories.map((cat, ci) => (
87
+ <div key={cat.category} className="animate-fade-in-up" style={{ animationDelay: `${ci * 0.1}s` }}>
88
+ <h3 className="font-bold mb-4">{cat.category}</h3>
89
+ <Accordion type="single" collapsible className="space-y-2">
90
+ {cat.questions.map((item, qi) => (
91
+ <AccordionItem
92
+ key={qi}
93
+ value={`${ci}-${qi}`}
94
+ className="rounded-xl border bg-card px-4 data-[state=open]:border-primary/20 transition-colors"
95
+ >
96
+ <AccordionTrigger className="text-sm font-medium hover:no-underline py-4">
97
+ {item.q}
98
+ </AccordionTrigger>
99
+ <AccordionContent className="text-sm text-muted-foreground leading-relaxed pb-4">
100
+ {item.a}
101
+ </AccordionContent>
102
+ </AccordionItem>
103
+ ))}
104
+ </Accordion>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ </section>
110
+ )
111
+ }
components/services/PackageComparison.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link"
2
+ import { Button } from "@/components/ui/button"
3
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
4
+ import { Check, X, ArrowRight, Sparkles, Star, Zap } from "lucide-react"
5
+
6
+ const tiers = [
7
+ {
8
+ name: "Starter",
9
+ tagline: "Perfect for small businesses",
10
+ price: "$2,999",
11
+ icon: Sparkles,
12
+ gradient: "from-blue-500 to-cyan-400",
13
+ popular: false,
14
+ features: [
15
+ { name: "Custom design (up to 5 pages)", included: true },
16
+ { name: "Responsive layout", included: true },
17
+ { name: "Contact form", included: true },
18
+ { name: "Basic SEO setup", included: true },
19
+ { name: "1 round of revisions", included: true },
20
+ { name: "CMS integration", included: false },
21
+ { name: "E-commerce functionality", included: false },
22
+ { name: "Priority support", included: false },
23
+ ],
24
+ },
25
+ {
26
+ name: "Professional",
27
+ tagline: "Most popular for growing brands",
28
+ price: "$7,499",
29
+ icon: Star,
30
+ gradient: "from-primary to-blue-500",
31
+ popular: true,
32
+ features: [
33
+ { name: "Custom design (up to 15 pages)", included: true },
34
+ { name: "Responsive layout", included: true },
35
+ { name: "Advanced forms & integrations", included: true },
36
+ { name: "Full SEO optimization", included: true },
37
+ { name: "3 rounds of revisions", included: true },
38
+ { name: "CMS integration", included: true },
39
+ { name: "E-commerce functionality", included: false },
40
+ { name: "Priority support", included: true },
41
+ ],
42
+ },
43
+ {
44
+ name: "Enterprise",
45
+ tagline: "For ambitious, large-scale projects",
46
+ price: "Custom",
47
+ icon: Zap,
48
+ gradient: "from-violet-600 to-purple-500",
49
+ popular: false,
50
+ features: [
51
+ { name: "Unlimited pages & features", included: true },
52
+ { name: "Responsive layout", included: true },
53
+ { name: "Custom integrations & APIs", included: true },
54
+ { name: "Full SEO optimization", included: true },
55
+ { name: "Unlimited revisions", included: true },
56
+ { name: "CMS integration", included: true },
57
+ { name: "E-commerce functionality", included: true },
58
+ { name: "Priority support", included: true },
59
+ ],
60
+ },
61
+ ]
62
+
63
+ export function PackageComparison() {
64
+ return (
65
+ <section className="py-24 bg-background" id="packages">
66
+ <div className="container">
67
+ <div className="animate-fade-in-up text-center mb-16">
68
+ <p className="text-overline text-primary mb-3">Pricing</p>
69
+ <h2 className="sm:text-4xl mb-4">Choose Your Package</h2>
70
+ <p className="text-body text-muted-foreground max-w-2xl mx-auto">
71
+ Transparent pricing with no hidden fees. Every package includes a dedicated project manager and quality assurance.
72
+ </p>
73
+ </div>
74
+
75
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
76
+ {tiers.map((tier, i) => (
77
+ <div key={tier.name} className="animate-fade-in-up" style={{ animationDelay: `${i * 0.1}s` }}>
78
+ <Card className={`relative h-full ${tier.popular ? "border-primary shadow-xl shadow-primary/10 scale-[1.02]" : "border"}`}>
79
+ {tier.popular && (
80
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary px-4 py-1 text-xs font-semibold text-primary-foreground">
81
+ Most Popular
82
+ </div>
83
+ )}
84
+ <CardHeader className="text-center pb-2">
85
+ <div className={`w-12 h-12 mx-auto rounded-xl bg-gradient-to-br ${tier.gradient} flex items-center justify-center text-white shadow-lg mb-4`}>
86
+ <tier.icon className="w-6 h-6" suppressHydrationWarning />
87
+ </div>
88
+ <h3 className="text-xl font-bold">{tier.name}</h3>
89
+ <p className="text-caption">{tier.tagline}</p>
90
+ <div className="mt-4">
91
+ <span className="text-4xl font-extrabold tracking-tight">{tier.price}</span>
92
+ {tier.price !== "Custom" && <span className="text-caption ml-1">starting</span>}
93
+ </div>
94
+ </CardHeader>
95
+ <CardContent className="space-y-6">
96
+ <ul className="space-y-3">
97
+ {tier.features.map((feature) => (
98
+ <li key={feature.name} className="flex items-center gap-3 text-sm">
99
+ {feature.included ? (
100
+ <Check className="h-4 w-4 text-green-500 shrink-0" suppressHydrationWarning />
101
+ ) : (
102
+ <X className="h-4 w-4 text-muted-foreground/40 shrink-0" suppressHydrationWarning />
103
+ )}
104
+ <span className={feature.included ? "" : "text-muted-foreground/60"}>
105
+ {feature.name}
106
+ </span>
107
+ </li>
108
+ ))}
109
+ </ul>
110
+ <Button asChild className="w-full" variant={tier.popular ? "default" : "outline"} size="lg">
111
+ <Link href="/quote">
112
+ Get Started
113
+ <ArrowRight className="ml-2 h-4 w-4" suppressHydrationWarning />
114
+ </Link>
115
+ </Button>
116
+ </CardContent>
117
+ </Card>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ </div>
122
+ </section>
123
+ )
124
+ }
components/services/ProcessTimeline.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Search, PenTool, Code, Rocket } from "lucide-react"
2
+
3
+ const steps = [
4
+ {
5
+ number: "01",
6
+ title: "Discovery",
7
+ description: "We dive deep into your business goals, target audience, and competition to build a solid project strategy.",
8
+ icon: Search,
9
+ gradient: "from-blue-500 to-cyan-400",
10
+ },
11
+ {
12
+ number: "02",
13
+ title: "Design",
14
+ description: "Our designers create wireframes and high-fidelity mockups that bring your vision to life with pixel-perfect precision.",
15
+ icon: PenTool,
16
+ gradient: "from-violet-500 to-purple-400",
17
+ },
18
+ {
19
+ number: "03",
20
+ title: "Develop",
21
+ description: "Our engineers build your product using modern tech stacks with clean code, performance, and scalability in mind.",
22
+ icon: Code,
23
+ gradient: "from-pink-500 to-rose-400",
24
+ },
25
+ {
26
+ number: "04",
27
+ title: "Deploy",
28
+ description: "We launch your project with rigorous QA testing, performance optimization, and ongoing support to ensure success.",
29
+ icon: Rocket,
30
+ gradient: "from-amber-500 to-orange-400",
31
+ },
32
+ ]
33
+
34
+ export function ProcessTimeline() {
35
+ return (
36
+ <section className="py-24 bg-background" id="process">
37
+ <div className="container">
38
+ <div className="animate-fade-in-up text-center mb-16">
39
+ <p className="text-overline text-primary mb-3">Our Process</p>
40
+ <h2 className="sm:text-4xl mb-4">How We Work</h2>
41
+ <p className="text-body text-muted-foreground max-w-2xl mx-auto">
42
+ A proven 4-step process that takes your project from idea to launch.
43
+ </p>
44
+ </div>
45
+
46
+ {/* Desktop: horizontal timeline */}
47
+ <div className="hidden md:grid md:grid-cols-4 gap-8 relative">
48
+ {/* Connecting line */}
49
+ <div className="absolute top-10 left-[12.5%] right-[12.5%] h-0.5 bg-border" aria-hidden="true" />
50
+
51
+ {steps.map((step, i) => (
52
+ <div key={step.number} className="animate-fade-in-up relative text-center" style={{ animationDelay: `${i * 0.12}s` }}>
53
+ <div className={`relative z-10 w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br ${step.gradient} flex items-center justify-center text-white shadow-lg mb-6`}>
54
+ <step.icon className="w-8 h-8" suppressHydrationWarning />
55
+ </div>
56
+ <div className="text-xs font-bold text-primary mb-1">{step.number}</div>
57
+ <h3 className="font-bold mb-2">{step.title}</h3>
58
+ <p className="text-caption leading-relaxed">{step.description}</p>
59
+ </div>
60
+ ))}
61
+ </div>
62
+
63
+ {/* Mobile: vertical timeline */}
64
+ <div className="md:hidden relative pl-12">
65
+ {/* Vertical line */}
66
+ <div className="absolute left-5 top-0 bottom-0 w-0.5 bg-border" aria-hidden="true" />
67
+
68
+ {steps.map((step, i) => (
69
+ <div key={step.number} className="animate-fade-in-up relative pb-12 last:pb-0" style={{ animationDelay: `${i * 0.12}s` }}>
70
+ <div className={`absolute left-[-1.75rem] z-10 w-10 h-10 rounded-xl bg-gradient-to-br ${step.gradient} flex items-center justify-center text-white shadow-lg`}>
71
+ <step.icon className="w-5 h-5" suppressHydrationWarning />
72
+ </div>
73
+ <div className="text-xs font-bold text-primary mb-1">{step.number}</div>
74
+ <h3 className="font-bold mb-2">{step.title}</h3>
75
+ <p className="text-caption leading-relaxed">{step.description}</p>
76
+ </div>
77
+ ))}
78
+ </div>
79
+ </div>
80
+ </section>
81
+ )
82
+ }
components/services/ServiceDescriptions.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
2
+ import { Code, Palette, Smartphone, LineChart, ArrowUpRight } from "lucide-react"
3
+ import Link from "next/link"
4
+
5
+ const services = [
6
+ {
7
+ title: "Web Development",
8
+ description: "We build fast, scalable web applications using Next.js, React, and Node.js. From marketing sites to complex SaaS platforms.",
9
+ icon: Code,
10
+ gradient: "from-blue-500 to-cyan-400",
11
+ deliverables: ["Custom Website", "Web Application", "API Development", "Database Design"],
12
+ },
13
+ {
14
+ title: "Mobile App Development",
15
+ description: "Cross-platform mobile apps for iOS and Android built with React Native and Flutter. Native performance, shared codebase.",
16
+ icon: Smartphone,
17
+ gradient: "from-violet-500 to-purple-400",
18
+ deliverables: ["iOS App", "Android App", "Cross-Platform", "App Store Deployment"],
19
+ },
20
+ {
21
+ title: "UI/UX Design",
22
+ description: "User-centric design that converts. We craft interfaces that are both beautiful and intuitive, backed by research and usability testing.",
23
+ icon: Palette,
24
+ gradient: "from-pink-500 to-rose-400",
25
+ deliverables: ["Wireframes", "Prototypes", "Design System", "Usability Testing"],
26
+ },
27
+ {
28
+ title: "SEO Optimization",
29
+ description: "Data-driven strategies to boost your search rankings, drive organic traffic, and increase visibility across all major search engines.",
30
+ icon: LineChart,
31
+ gradient: "from-amber-500 to-orange-400",
32
+ deliverables: ["Technical Audit", "Keyword Research", "On-Page SEO", "Analytics Setup"],
33
+ },
34
+ ]
35
+
36
+ export function ServiceDescriptions() {
37
+ return (
38
+ <section className="py-24 bg-muted/20" id="services-detail">
39
+ <div className="container">
40
+ <div className="animate-fade-in-up text-center mb-16">
41
+ <p className="text-overline text-primary mb-3">What We Do</p>
42
+ <h2 className="sm:text-4xl mb-4">Our Services</h2>
43
+ <p className="text-body text-muted-foreground max-w-2xl mx-auto">
44
+ End-to-end digital solutions tailored to your business goals.
45
+ </p>
46
+ </div>
47
+
48
+ <div className="grid gap-8 md:grid-cols-2">
49
+ {services.map((service, i) => (
50
+ <div key={service.title} className="animate-fade-in-up" style={{ animationDelay: `${i * 0.1}s` }}>
51
+ <Card className="group h-full border hover:border-primary/30 hover:shadow-xl hover:shadow-primary/5 transition-all duration-500">
52
+ <CardHeader>
53
+ <div className="flex items-start justify-between">
54
+ <div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${service.gradient} flex items-center justify-center text-white shadow-lg group-hover:scale-110 transition-transform duration-300`}>
55
+ <service.icon className="w-7 h-7" suppressHydrationWarning />
56
+ </div>
57
+ <Link
58
+ href="/quote"
59
+ className="opacity-0 group-hover:opacity-100 transition-opacity duration-300"
60
+ aria-label={`Request a quote for ${service.title}`}
61
+ >
62
+ <ArrowUpRight className="h-5 w-5 text-primary" suppressHydrationWarning />
63
+ </Link>
64
+ </div>
65
+ <h3 className="text-xl font-bold mt-4">{service.title}</h3>
66
+ </CardHeader>
67
+ <CardContent className="space-y-4">
68
+ <p className="text-body text-muted-foreground">{service.description}</p>
69
+ <div>
70
+ <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">Key Deliverables</p>
71
+ <div className="flex flex-wrap gap-2">
72
+ {service.deliverables.map((d) => (
73
+ <span key={d} className="bg-secondary text-secondary-foreground px-3 py-1 rounded-full text-xs font-medium">
74
+ {d}
75
+ </span>
76
+ ))}
77
+ </div>
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </div>
85
+ </section>
86
+ )
87
+ }
components/theme-provider.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
5
+
6
+ export function ThemeProvider({
7
+ children,
8
+ ...props
9
+ }: React.ComponentProps<typeof NextThemesProvider>) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronDownIcon } from "lucide-react"
5
+ import { Accordion as AccordionPrimitive } from "radix-ui"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }