diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8116248..f8fb191 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,27 +1,27 @@ name: Playwright Tests on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm install -g pnpm && pnpm install - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps - - name: Run Playwright tests - run: pnpm exec playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/app/dashboard/company/page.tsx b/app/dashboard/company/page.tsx index 567ae71..2e4df21 100644 --- a/app/dashboard/company/page.tsx +++ b/app/dashboard/company/page.tsx @@ -130,7 +130,9 @@ export default function CompanySettingsPage() { {message && ( - + {message} )} @@ -147,7 +149,9 @@ export default function CompanySettingsPage() {
- Data Source Configuration + + Data Source Configuration +
diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx index 3df6a23..46bb3f4 100644 --- a/app/dashboard/overview/page.tsx +++ b/app/dashboard/overview/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback, useRef } from "react"; +import { useEffect, useState } from "react"; import { signOut, useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { Company, MetricsResult, WordCloudWord } from "../../../lib/types"; @@ -13,7 +13,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; -import { Separator } from "@/components/ui/separator"; import { DropdownMenu, DropdownMenuContent, @@ -30,7 +29,6 @@ import { CheckCircle, RefreshCw, LogOut, - Calendar, MoreVertical, Globe, MessageCircle, @@ -38,7 +36,6 @@ import { import WordCloud from "../../../components/WordCloud"; import GeographicMap from "../../../components/GeographicMap"; import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution"; -import DateRangePicker from "../../../components/DateRangePicker"; import TopQuestionsChart from "../../../components/TopQuestionsChart"; // Safely wrapped component with useSession @@ -49,12 +46,6 @@ function DashboardContent() { const [company, setCompany] = useState(null); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); - const [dateRange, setDateRange] = useState<{ - minDate: string; - maxDate: string; - } | null>(null); - const [selectedStartDate, setSelectedStartDate] = useState(""); - const [selectedEndDate, setSelectedEndDate] = useState(""); const [isInitialLoad, setIsInitialLoad] = useState(true); const isAuditor = session?.user?.role === "AUDITOR"; @@ -78,11 +69,8 @@ function DashboardContent() { setMetrics(data.metrics); setCompany(data.company); - // Set date range from API response (only on initial load) - if (data.dateRange && isInitial) { - setDateRange(data.dateRange); - setSelectedStartDate(data.dateRange.minDate); - setSelectedEndDate(data.dateRange.maxDate); + // Set initial load flag + if (isInitial) { setIsInitialLoad(false); } } catch (error) { @@ -92,16 +80,6 @@ function DashboardContent() { } }; - // Handle date range changes - const handleDateRangeChange = useCallback( - (startDate: string, endDate: string) => { - setSelectedStartDate(startDate); - setSelectedEndDate(endDate); - fetchMetrics(startDate, endDate); - }, - [] - ); - useEffect(() => { // Redirect if not authenticated if (status === "unauthenticated") { @@ -263,7 +241,10 @@ function DashboardContent() { return Object.entries(metrics.categories).map(([name, value]) => { const formattedName = formatEnumValue(name) || name; return { - name: formattedName.length > 15 ? formattedName.substring(0, 15) + "..." : formattedName, + name: + formattedName.length > 15 + ? formattedName.substring(0, 15) + "..." + : formattedName, value: value as number, }; }); @@ -337,24 +318,36 @@ function DashboardContent() { disabled={refreshing || isAuditor} size="sm" className="gap-2" + aria-label={ + refreshing + ? "Refreshing dashboard data" + : "Refresh dashboard data" + } + aria-describedby={refreshing ? "refresh-status" : undefined} > @@ -244,7 +252,7 @@ export default function SessionViewPage() {

User ID

- {session.userId || 'N/A'} + {session.userId || "N/A"}

@@ -260,9 +268,11 @@ export default function SessionViewPage() {

{session.endTime && session.startTime ? `${Math.round( - (new Date(session.endTime).getTime() - new Date(session.startTime).getTime()) / 60000 + (new Date(session.endTime).getTime() - + new Date(session.startTime).getTime()) / + 60000 )} min` - : 'N/A'} + : "N/A"}

@@ -302,9 +312,10 @@ export default function SessionViewPage() { href={session.fullTranscriptUrl} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-2 text-primary hover:underline" + className="inline-flex items-center gap-2 text-primary hover:underline focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-sm" + aria-label="Open original transcript in new tab" > - +
diff --git a/app/dashboard/sessions/page.tsx b/app/dashboard/sessions/page.tsx index 731d4e4..7951e55 100644 --- a/app/dashboard/sessions/page.tsx +++ b/app/dashboard/sessions/page.tsx @@ -9,18 +9,17 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { formatCategory } from "@/lib/format-enums"; -import { - MessageSquare, - Search, - Filter, - Calendar, - ChevronLeft, +import { + MessageSquare, + Search, + Filter, + ChevronLeft, ChevronRight, Clock, Globe, Eye, ChevronDown, - ChevronUp + ChevronUp, } from "lucide-react"; // Placeholder for a SessionListItem component to be created later @@ -145,7 +144,7 @@ export default function SessionsPage() {
{/* Page heading for screen readers */}

Sessions Management

- + {/* Header */} @@ -158,11 +157,16 @@ export default function SessionsPage() { {/* Search Input */}
-

Search Sessions

+

+ Search Sessions +

-
- - {filtersExpanded && ( - -
- Session Filters and Sorting Options -
- {/* Category Filter */} -
- - setSelectedCategory(e.target.value)} + aria-describedby="category-help" + > + + {filterOptions.categories.map((cat) => ( + + ))} + +
+ Filter sessions by category type +
+
+ + {/* Language Filter */} +
+ + +
+ Filter sessions by language +
+
+ + {/* Start Date Filter */} +
+ + setStartDate(e.target.value)} + aria-describedby="start-date-help" + /> +
+ Filter sessions from this date onwards +
+
+ + {/* End Date Filter */} +
+ + setEndDate(e.target.value)} + aria-describedby="end-date-help" + /> +
+ Filter sessions up to this date +
+
+ + {/* Sort Key */} +
+ + -
- Filter sessions by category type + +
+ Choose field to sort sessions by +
-
- {/* Language Filter */} -
- - -
- Filter sessions by language + {/* Sort Order */} +
+ + +
+ Choose ascending or descending order +
- - {/* Start Date Filter */} -
- - setStartDate(e.target.value)} - aria-describedby="start-date-help" - /> -
- Filter sessions from this date onwards -
-
- - {/* End Date Filter */} -
- - setEndDate(e.target.value)} - aria-describedby="end-date-help" - /> -
- Filter sessions up to this date -
-
- - {/* Sort Key */} -
- - -
- Choose field to sort sessions by -
-
- - {/* Sort Order */} -
- - -
- Choose ascending or descending order -
-
-
-
-
- )} + +
+ )}
{/* Results section */}
-

Session Results

- +

+ Session Results +

+ {/* Live region for screen reader announcements */} -
- {loading && "Loading sessions..."} - {error && `Error loading sessions: ${error}`} - {!loading && !error && sessions.length > 0 && `Found ${sessions.length} sessions`} - {!loading && !error && sessions.length === 0 && "No sessions found"} -
+
+ {loading && "Loading sessions..."} + {error && `Error loading sessions: ${error}`} + {!loading && + !error && + sessions.length > 0 && + `Found ${sessions.length} sessions`} + {!loading && !error && sessions.length === 0 && "No sessions found"} +
- {/* Loading State */} - {loading && ( - - - - - - )} + {/* Loading State */} + {loading && ( + + + + + + )} - {/* Error State */} - {error && ( - - - - - - )} + {/* Error State */} + {error && ( + + + + + + )} - {/* Empty State */} - {!loading && !error && sessions.length === 0 && ( - - -
- {debouncedSearchTerm - ? `No sessions found for "${debouncedSearchTerm}".` - : "No sessions found."} -
-
-
- )} + {/* Empty State */} + {!loading && !error && sessions.length === 0 && ( + + +
+ {debouncedSearchTerm + ? `No sessions found for "${debouncedSearchTerm}".` + : "No sessions found."} +
+
+
+ )} {/* Sessions List */} {!loading && !error && sessions.length > 0 && ( @@ -388,11 +412,18 @@ export default function SessionsPage() {
-

- Session {session.sessionId || session.id} from {new Date(session.startTime).toLocaleDateString()} +

+ Session {session.sessionId || session.id} from{" "} + {new Date(session.startTime).toLocaleDateString()}

- + ID @@ -401,7 +432,10 @@ export default function SessionsPage() {
- @@ -410,14 +444,16 @@ export default function SessionsPage() {
-
@@ -454,38 +490,40 @@ export default function SessionsPage() { )} - {/* Pagination */} - {totalPages > 0 && ( - - -
- - - Page {currentPage} of {totalPages} - - -
-
-
- )} + {/* Pagination */} + {totalPages > 0 && ( + + +
+ + + Page {currentPage} of {totalPages} + + +
+
+
+ )}
); diff --git a/app/dashboard/users/page.tsx b/app/dashboard/users/page.tsx index e331d67..897a73f 100644 --- a/app/dashboard/users/page.tsx +++ b/app/dashboard/users/page.tsx @@ -2,6 +2,28 @@ import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Users, UserPlus, Shield, Eye, AlertCircle } from "lucide-react"; interface UserItem { id: string; @@ -13,15 +35,21 @@ export default function UserManagementPage() { const { data: session, status } = useSession(); const [users, setUsers] = useState([]); const [email, setEmail] = useState(""); - const [role, setRole] = useState("user"); + const [role, setRole] = useState("USER"); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(true); useEffect(() => { if (status === "authenticated") { - fetchUsers(); + if (session?.user?.role === "ADMIN") { + fetchUsers(); + } else { + setLoading(false); // Stop loading for non-admin users + } + } else if (status === "unauthenticated") { + setLoading(false); } - }, [status]); + }, [status, session?.user?.role]); const fetchUsers = async () => { setLoading(true); @@ -65,148 +93,181 @@ export default function UserManagementPage() { // Loading state if (loading) { - return
Loading users...
; + return ( +
+ + +
+ Loading users... +
+
+
+
+ ); } // Check for admin access if (session?.user?.role !== "ADMIN") { return ( -
-

Access Denied

-

You don't have permission to view user management.

+
+ + +
+ +

+ Access Denied +

+

+ You don't have permission to view user management. +

+
+
+
); } return ( -
-
-

- User Management -

+
+ {/* Header */} + + + + + User Management + + + - {message && ( -
- {message} -
- )} + {/* Message Alert */} + {message && ( + + {message} + + )} -
-

Invite New User

+ {/* Invite New User */} + + + + + Invite New User + + +
{ e.preventDefault(); inviteUser(); }} - autoComplete="off" // Disable autofill for the form + autoComplete="off" + data-testid="invite-form" + role="form" > -
- - + + setEmail(e.target.value)} required - autoComplete="off" // Disable autofill for this input + autoComplete="off" />
-
- - +
+ +
- + -
+
+
-
-

Current Users

+ {/* Current Users */} + + + + + Current Users ({users?.length || 0}) + + +
- - - - - - - - - +
- Email - - Role - - Actions -
+ + + Email + Role + Actions + + + {users.length === 0 ? ( - - - + + ) : ( users.map((user) => ( - - - - - + + )) )} - -
+ No users found -
+ + {user.email} - - + + + {user.role === "ADMIN" && ( + + )} + {user.role === "AUDITOR" && ( + + )} {user.role} - - - {/* For future: Add actions like edit, delete, etc. */} - + + + + No actions available -
+ +
-
-
+ +
); } diff --git a/app/globals.css b/app/globals.css index 936cc22..5cae55d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -41,56 +41,76 @@ --color-sidebar-ring: var(--sidebar-ring); --animate-shine: shine var(--duration) infinite linear; @keyframes shine { - 0% { - background-position: 0% 0%; + 0% { + background-position: 0% 0%; } - 50% { - background-position: 100% 100%; + 50% { + background-position: 100% 100%; } - to { - background-position: 0% 0%; + to { + background-position: 0% 0%; } } - --animate-meteor: meteor 5s linear infinite -; + --animate-meteor: meteor 5s linear infinite; @keyframes meteor { - 0% { - transform: rotate(var(--angle)) translateX(0); - opacity: 1;} - 70% { - opacity: 1;} - 100% { - transform: rotate(var(--angle)) translateX(-500px); - opacity: 0;}} - --animate-background-position-spin: background-position-spin 3000ms infinite alternate; + 0% { + transform: rotate(var(--angle)) translateX(0); + opacity: 1; + } + 70% { + opacity: 1; + } + 100% { + transform: rotate(var(--angle)) translateX(-500px); + opacity: 0; + } + } + --animate-background-position-spin: background-position-spin 3000ms infinite + alternate; @keyframes background-position-spin { - 0% { - background-position: top center;} - 100% { - background-position: bottom center;}} + 0% { + background-position: top center; + } + 100% { + background-position: bottom center; + } + } --animate-aurora: aurora 8s ease-in-out infinite alternate; @keyframes aurora { - 0% { - background-position: 0% 50%; - transform: rotate(-5deg) scale(0.9);} - 25% { - background-position: 50% 100%; - transform: rotate(5deg) scale(1.1);} - 50% { - background-position: 100% 50%; - transform: rotate(-3deg) scale(0.95);} - 75% { - background-position: 50% 0%; - transform: rotate(3deg) scale(1.05);} - 100% { - background-position: 0% 50%; - transform: rotate(-5deg) scale(0.9);}} + 0% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); + } + 25% { + background-position: 50% 100%; + transform: rotate(5deg) scale(1.1); + } + 50% { + background-position: 100% 50%; + transform: rotate(-3deg) scale(0.95); + } + 75% { + background-position: 50% 0%; + transform: rotate(3deg) scale(1.05); + } + 100% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); + } + } --animate-shiny-text: shiny-text 8s infinite; @keyframes shiny-text { - 0%, 90%, 100% { - background-position: calc(-100% - var(--shiny-width)) 0;} - 30%, 60% { - background-position: calc(100% + var(--shiny-width)) 0;}}} + 0%, + 90%, + 100% { + background-position: calc(-100% - var(--shiny-width)) 0; + } + 30%, + 60% { + background-position: calc(100% + var(--shiny-width)) 0; + } + } +} :root { --radius: 0.625rem; @@ -168,7 +188,7 @@ body { @apply bg-background text-foreground; } - + /* Line clamp utility */ .line-clamp-2 { display: -webkit-box; @@ -176,4 +196,4 @@ -webkit-box-orient: vertical; overflow: hidden; } -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index 9b209dc..74fba80 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -23,8 +23,8 @@ export default function RootLayout({ children }: { children: ReactNode }) { {/* Skip navigation link for keyboard users */} - Skip to main content diff --git a/app/login/page.tsx b/app/login/page.tsx index b07eb3b..95d5b3f 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -4,7 +4,13 @@ import { signIn } from "next-auth/react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -73,7 +79,8 @@ export default function LoginPage() { Welcome back to your analytics dashboard

- Monitor, analyze, and optimize your customer conversations with AI-powered insights. + Monitor, analyze, and optimize your customer conversations with + AI-powered insights.

@@ -81,19 +88,25 @@ export default function LoginPage() {
- Real-time analytics and insights + + Real-time analytics and insights +
- Enterprise-grade security + + Enterprise-grade security +
- AI-powered conversation analysis + + AI-powered conversation analysis +
@@ -130,13 +143,19 @@ export default function LoginPage() { + {/* Live region for screen reader announcements */} +
+ {isLoading && "Signing in, please wait..."} + {error && `Error: ${error}`} +
+ {error && ( - + {error} )} -
+
setEmail(e.target.value)} disabled={isLoading} required + aria-describedby="email-help" + aria-invalid={!!error} className="transition-all duration-200 focus:ring-2 focus:ring-primary/20" /> +
+ Enter your company email address +
@@ -160,39 +184,57 @@ export default function LoginPage() { onChange={(e) => setPassword(e.target.value)} disabled={isLoading} required + aria-describedby="password-help" + aria-invalid={!!error} className="transition-all duration-200 focus:ring-2 focus:ring-primary/20" /> +
+ Enter your account password +
+ {isLoading && ( +
+ Authentication in progress, please wait +
+ )}
- Don't have a company account? Register here + Don't have a company account? Register here
Forgot your password? @@ -203,11 +245,17 @@ export default function LoginPage() {

By signing in, you agree to our{" "} - + Terms of Service {" "} and{" "} - + Privacy Policy

diff --git a/components/Map.tsx b/components/Map.tsx index b95f232..7e83c99 100644 --- a/components/Map.tsx +++ b/components/Map.tsx @@ -36,7 +36,7 @@ const Map = ({ countryData, maxCount }: MapProps) => { const tileLayerUrl = isDark ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" : "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"; - + const tileLayerAttribution = isDark ? '©
OpenStreetMap contributors © CARTO' : '© OpenStreetMap contributors © CARTO'; @@ -49,10 +49,7 @@ const Map = ({ countryData, maxCount }: MapProps) => { scrollWheelZoom={false} style={{ height: "100%", width: "100%", borderRadius: "0.5rem" }} > - + {countryData.map((country) => ( {
{getLocalizedCountryName(country.code)}
-
Sessions: {country.count}
+
+ Sessions: {country.count} +
diff --git a/components/ResponseTimeDistribution.tsx b/components/ResponseTimeDistribution.tsx index a1417a4..7d03627 100644 --- a/components/ResponseTimeDistribution.tsx +++ b/components/ResponseTimeDistribution.tsx @@ -114,9 +114,9 @@ export default function ResponseTimeDistribution({ /> } /> - diff --git a/components/SessionDetails.tsx b/components/SessionDetails.tsx index e54878a..94a934b 100644 --- a/components/SessionDetails.tsx +++ b/components/SessionDetails.tsx @@ -97,7 +97,8 @@ export default function SessionDetails({ session }: SessionDetailsProps) { : "secondary" } > - {session.sentiment.charAt(0).toUpperCase() + session.sentiment.slice(1)} + {session.sentiment.charAt(0).toUpperCase() + + session.sentiment.slice(1)}
)} @@ -107,12 +108,17 @@ export default function SessionDetails({ session }: SessionDetailsProps) {

{session.messagesSent || 0}

- {session.avgResponseTime !== null && session.avgResponseTime !== undefined && ( -
-

Avg Response Time

-

{session.avgResponseTime.toFixed(2)}s

-
- )} + {session.avgResponseTime !== null && + session.avgResponseTime !== undefined && ( +
+

+ Avg Response Time +

+

+ {session.avgResponseTime.toFixed(2)}s +

+
+ )} {session.escalated !== null && session.escalated !== undefined && (
@@ -123,14 +129,19 @@ export default function SessionDetails({ session }: SessionDetailsProps) {
)} - {session.forwardedHr !== null && session.forwardedHr !== undefined && ( -
-

Forwarded to HR

- - {session.forwardedHr ? "Yes" : "No"} - -
- )} + {session.forwardedHr !== null && + session.forwardedHr !== undefined && ( +
+

+ Forwarded to HR +

+ + {session.forwardedHr ? "Yes" : "No"} + +
+ )} {session.ipAddress && (
@@ -156,7 +167,9 @@ export default function SessionDetails({ session }: SessionDetailsProps) { {!session.summary && session.initialMsg && (
-

Initial Message

+

+ Initial Message +

"{session.initialMsg}"
@@ -171,9 +184,10 @@ export default function SessionDetails({ session }: SessionDetailsProps) { href={session.fullTranscriptUrl} target="_blank" rel="noopener noreferrer" - className="inline-flex items-center gap-2 text-primary hover:underline" + className="inline-flex items-center gap-2 text-primary hover:underline focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-sm" + aria-label="Open full transcript in new tab" > - +
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 395ea2f..cd62647 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -337,8 +337,14 @@ export default function Sidebar({
{/* Theme Toggle */} -
- {isExpanded && Theme} +
+ {isExpanded && ( + + Theme + + )}
diff --git a/components/charts/donut-chart.tsx b/components/charts/donut-chart.tsx index d7dfbc0..e658ce6 100644 --- a/components/charts/donut-chart.tsx +++ b/components/charts/donut-chart.tsx @@ -92,7 +92,11 @@ export default function ModernDonutChart({ )} -
+
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + } + }} > {dataWithTotal.map((entry, index) => ( diff --git a/components/magicui/animated-beam.tsx b/components/magicui/animated-beam.tsx index eefe1c4..6828599 100644 --- a/components/magicui/animated-beam.tsx +++ b/components/magicui/animated-beam.tsx @@ -130,7 +130,7 @@ export const AnimatedBeam: React.FC = ({ xmlns="http://www.w3.org/2000/svg" className={cn( "pointer-events-none absolute left-0 top-0 transform-gpu stroke-2", - className, + className )} viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`} > diff --git a/components/magicui/animated-shiny-text.tsx b/components/magicui/animated-shiny-text.tsx index 7a8506b..e010816 100644 --- a/components/magicui/animated-shiny-text.tsx +++ b/components/magicui/animated-shiny-text.tsx @@ -29,7 +29,7 @@ export const AnimatedShinyText: FC = ({ // Shine gradient "bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80", - className, + className )} {...props} > diff --git a/components/magicui/aurora-text.tsx b/components/magicui/aurora-text.tsx index 4b37963..8fc3a43 100644 --- a/components/magicui/aurora-text.tsx +++ b/components/magicui/aurora-text.tsx @@ -37,7 +37,7 @@ export const AuroraText = memo( ); - }, + } ); AuroraText.displayName = "AuroraText"; diff --git a/components/magicui/border-beam.tsx b/components/magicui/border-beam.tsx index c9ca066..6886c74 100644 --- a/components/magicui/border-beam.tsx +++ b/components/magicui/border-beam.tsx @@ -66,15 +66,17 @@ export const BorderBeam = ({ return (
((props, ref) => { } } }, - [globalOptions], + [globalOptions] ); const fire = useCallback( @@ -71,14 +71,14 @@ const ConfettiComponent = forwardRef((props, ref) => { console.error("Confetti error:", error); } }, - [options], + [options] ); const api = useMemo( () => ({ fire, }), - [fire], + [fire] ); useImperativeHandle(ref, () => api, [api]); diff --git a/components/magicui/magic-card.tsx b/components/magicui/magic-card.tsx index 58b71ef..48a0a05 100644 --- a/components/magicui/magic-card.tsx +++ b/components/magicui/magic-card.tsx @@ -38,7 +38,7 @@ export function MagicCard({ mouseY.set(clientY - top); } }, - [mouseX, mouseY], + [mouseX, mouseY] ); const handleMouseOut = useCallback( @@ -49,7 +49,7 @@ export function MagicCard({ mouseY.set(-gradientSize); } }, - [handleMouseMove, mouseX, gradientSize, mouseY], + [handleMouseMove, mouseX, gradientSize, mouseY] ); const handleMouseEnter = useCallback(() => { diff --git a/components/magicui/meteors.tsx b/components/magicui/meteors.tsx index de2c61b..6593e99 100644 --- a/components/magicui/meteors.tsx +++ b/components/magicui/meteors.tsx @@ -23,7 +23,7 @@ export const Meteors = ({ className, }: MeteorsProps) => { const [meteorStyles, setMeteorStyles] = useState>( - [], + [] ); useEffect(() => { @@ -48,7 +48,7 @@ export const Meteors = ({ style={{ ...style }} className={cn( "pointer-events-none absolute size-0.5 rotate-[var(--angle)] animate-meteor rounded-full bg-zinc-500 shadow-[0_0_0_1px_#ffffff10]", - className, + className )} > {/* Meteor Tail */} diff --git a/components/magicui/neon-gradient-card.tsx b/components/magicui/neon-gradient-card.tsx index 1a2a76b..1892960 100644 --- a/components/magicui/neon-gradient-card.tsx +++ b/components/magicui/neon-gradient-card.tsx @@ -124,7 +124,7 @@ export const NeonGradientCard: React.FC = ({ } className={cn( "relative z-10 size-full rounded-[var(--border-radius)]", - className, + className )} {...props} > @@ -139,7 +139,7 @@ export const NeonGradientCard: React.FC = ({ "after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']", "after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80", "after:animate-background-position-spin", - "dark:bg-neutral-900", + "dark:bg-neutral-900" )} > {children} diff --git a/components/magicui/number-ticker.tsx b/components/magicui/number-ticker.tsx index 1b9bf39..a8a621e 100644 --- a/components/magicui/number-ticker.tsx +++ b/components/magicui/number-ticker.tsx @@ -49,7 +49,7 @@ export function NumberTicker({ }).format(Number(latest.toFixed(decimalPlaces))); } }), - [springValue, decimalPlaces], + [springValue, decimalPlaces] ); return ( @@ -57,7 +57,7 @@ export function NumberTicker({ ref={ref} className={cn( "inline-block tabular-nums tracking-wider text-black dark:text-white", - className, + className )} {...props} > diff --git a/components/magicui/pointer.tsx b/components/magicui/pointer.tsx index be75d4e..309d9ee 100644 --- a/components/magicui/pointer.tsx +++ b/components/magicui/pointer.tsx @@ -9,7 +9,9 @@ import { } from "motion/react"; import { useEffect, useRef, useState } from "react"; -interface PointerProps extends Omit, "ref"> {} +interface PointerProps extends Omit, "ref"> { + children?: React.ReactNode; +} /** * A custom pointer component that displays an animated cursor. @@ -104,7 +106,7 @@ export function Pointer({ xmlns="http://www.w3.org/2000/svg" className={cn( "rotate-[-70deg] stroke-white text-black", - className, + className )} > diff --git a/components/magicui/scroll-progress.tsx b/components/magicui/scroll-progress.tsx index 3edfe6f..3b12eee 100644 --- a/components/magicui/scroll-progress.tsx +++ b/components/magicui/scroll-progress.tsx @@ -4,7 +4,9 @@ import { cn } from "@/lib/utils"; import { motion, MotionProps, useScroll } from "motion/react"; import React from "react"; interface ScrollProgressProps - extends Omit, keyof MotionProps> {} + extends Omit, keyof MotionProps> { + className?: string; +} export const ScrollProgress = React.forwardRef< HTMLDivElement, @@ -17,7 +19,7 @@ export const ScrollProgress = React.forwardRef< ref={ref} className={cn( "fixed inset-x-0 top-0 z-50 h-px origin-left bg-gradient-to-r from-[#A97CF8] via-[#F38CB8] to-[#FDCC92]", - className, + className )} style={{ scaleX: scrollYProgress, diff --git a/components/magicui/shine-border.tsx b/components/magicui/shine-border.tsx index 45b1c41..723f6aa 100644 --- a/components/magicui/shine-border.tsx +++ b/components/magicui/shine-border.tsx @@ -55,7 +55,7 @@ export function ShineBorder({ } className={cn( "pointer-events-none absolute inset-0 size-full rounded-[inherit] will-change-[background-position] motion-safe:animate-shine", - className, + className )} {...props} /> diff --git a/components/magicui/text-animate.tsx b/components/magicui/text-animate.tsx index dc988dd..d2aa238 100644 --- a/components/magicui/text-animate.tsx +++ b/components/magicui/text-animate.tsx @@ -395,7 +395,7 @@ const TextAnimateBase = ({ className={cn( by === "line" ? "block" : "inline-block whitespace-pre", by === "character" && "", - segmentClassName, + segmentClassName )} > {segment} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 7afe882..b0ff266 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -6,4 +6,4 @@ import { type ThemeProviderProps } from "next-themes/dist/types"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children}; -} \ No newline at end of file +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..ec141d1 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Accordion({ + ...props +}: React.ComponentProps) { + return ; +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..930bf08 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx index f002e4c..b068396 100644 --- a/components/ui/alert.tsx +++ b/components/ui/alert.tsx @@ -56,4 +56,4 @@ const AlertDescription = React.forwardRef< )); AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription }; \ No newline at end of file +export { Alert, AlertTitle, AlertDescription }; diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..9d88a37 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return