Compare commits

..
1 Commits
14 changed files with 448 additions and 575 deletions

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
<svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
<stop offset="50%" style="stop-color:#0369a1;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main rounded shape background -->
<path d="M 150 80 Q 350 50 450 150 L 480 300 Q 500 450 380 550 Q 200 650 100 550 Q 30 450 50 300 L 80 150 Q 100 80 150 80 Z" fill="url(#logoGrad)"/>
<!-- Arrow pointing up-right (top right) -->
<g transform="translate(340, 100)">
<path d="M 0 80 L 0 0 M 0 0 L -25 25 M 0 0 L 25 25" stroke="white" stroke-width="20" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Hand holding dumbbell (left side) -->
<g transform="translate(80, 180)">
<!-- Upper arm -->
<ellipse cx="40" cy="30" rx="25" ry="60" fill="white" opacity="0.95" transform="rotate(-30 40 30)"/>
<!-- Forearm and hand -->
<path d="M 60 80 Q 100 100 140 85" stroke="white" stroke-width="35" fill="none" stroke-linecap="round" opacity="0.95"/>
<!-- Fingers -->
<circle cx="145" cy="75" r="15" fill="white" opacity="0.95"/>
<circle cx="155" cy="90" r="12" fill="white" opacity="0.95"/>
<circle cx="150" cy="105" r="13" fill="white" opacity="0.95"/>
<!-- Dumbbell bar -->
<rect x="155" y="75" width="110" height="28" rx="14" fill="white" opacity="0.95"/>
<!-- Left weight plate -->
<circle cx="155" cy="89" r="45" fill="white" opacity="0.95"/>
<circle cx="155" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
<rect x="145" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
<!-- Right weight plate -->
<circle cx="265" cy="89" r="45" fill="white" opacity="0.95"/>
<circle cx="265" cy="89" r="38" fill="url(#logoGrad)" opacity="0.4"/>
<rect x="255" y="130" width="20" height="30" fill="white" opacity="0.95" rx="3"/>
</g>
<!-- Text at bottom -->
<text x="300" y="630" font-size="85" font-weight="900" fill="url(#logoGrad)" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" letter-spacing="2">NextForm</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+35
View File
@@ -0,0 +1,35 @@
<svg viewBox="0 0 600 700" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main Shape Background -->
<path d="M 150 150 L 350 150 L 450 250 L 450 450 L 350 550 L 150 550 L 50 450 L 50 250 Z" fill="url(logoGradient)" opacity="0.15"/>
<!-- Dumbbell Icon -->
<g transform="translate(200, 200)">
<!-- Left Weight -->
<circle cx="20" cy="60" r="35" fill="url(logoGradient)"/>
<rect x="10" y="70" width="20" height="80" fill="url(logoGradient)"/>
<!-- Bar -->
<rect x="35" y="100" width="130" height="25" rx="12" fill="url(logoGradient)"/>
<!-- Right Weight -->
<circle cx="180" cy="60" r="35" fill="url(logoGradient)"/>
<rect x="170" y="70" width="20" height="80" fill="url(logoGradient)"/>
<!-- Hand -->
<ellipse cx="80" cy="20" rx="25" ry="30" fill="url(logoGradient)"/>
<path d="M 70 0 Q 65 -15 60 -20" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
<path d="M 85 -5 Q 88 -20 90 -28" stroke="url(logoGradient)" stroke-width="8" fill="none" stroke-linecap="round"/>
</g>
<!-- Arrow/Growth -->
<g transform="translate(320, 150)">
<path d="M 20 100 L 80 40 L 80 70 L 120 30 L 80 30 L 80 0 Z" fill="url(logoGradient)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1 -1
View File
@@ -26,7 +26,7 @@ export default function RootLayout({
<ClerkProvider> <ClerkProvider>
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className={inter.className}>
<div className="flex min-h-screen bg-gradient-to-br from-white via-blue-50 to-slate-50"> <div className="flex min-h-screen bg-slate-50">
<Sidebar /> <Sidebar />
<main className="flex-1 ml-64 p-8"> <main className="flex-1 ml-64 p-8">
{children} {children}
+28 -41
View File
@@ -1,10 +1,10 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Users, Activity, Zap, TrendingUp, Trophy, Target } from "lucide-react"; import { Users, CreditCard, CalendarCheck, TrendingUp } from "lucide-react";
import { StatsCard } from "@/components/ui/StatsCard"; import { StatsCard } from "@/components/ui/StatsCard";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import { UserManagement } from "@/components/users/UserManagement"; import { UserManagement } from "@/components/users/UserManagement";
import { AnalyticsDashboard } from "@/components/analytics/AnalyticsDashboard";
import axios from "axios"; import axios from "axios";
interface DashboardStats { interface DashboardStats {
@@ -46,39 +46,27 @@ export default function Home() {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-white via-slate-50 to-blue-50"> <div className="space-y-8">
{/* Premium Header */} {/* Header with Logo and App Name */}
<div className="bg-gradient-to-r from-slate-900 via-blue-900 to-slate-900 shadow-2xl"> <div className="flex items-center gap-4 pb-6 border-b border-slate-200">
<div className="max-w-7xl mx-auto px-8 py-12"> <div className="w-20 h-20">
<div className="flex items-end justify-between"> <img src="/nextform-logo.png" alt="NextForm Logo" className="w-full h-full object-contain" />
<div className="flex items-end gap-6">
<div className="w-16 h-16 bg-gradient-to-br from-blue-400 to-cyan-400 rounded-3xl flex items-center justify-center shadow-lg">
<Trophy className="w-9 h-9 text-white" />
</div> </div>
<div> <div>
<h1 className="text-5xl font-black text-white tracking-tight">FitAI</h1> <h1 className="text-4xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
<p className="text-blue-200 text-sm font-semibold mt-2">Premium Sports Management Platform</p> NextForm
</div> </h1>
</div> <p className="text-sm text-slate-500 font-medium">Fitness Management Platform</p>
<div className="text-right">
<p className="text-blue-100 text-sm">Welcome</p>
<p className="text-2xl font-bold text-white">Admin Control</p>
</div>
</div>
</div> </div>
</div> </div>
{/* Main Content with Proper Spacing */} {/* Performance Metrics Section */}
<div className="max-w-7xl mx-auto px-8 py-16">
{/* Section: Performance Metrics */}
<div className="mb-20"> <div className="mb-20">
<div className="mb-10"> <div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2> <h2 className="text-3xl font-black text-slate-900">Performance Metrics</h2>
<p className="text-slate-600 mt-2 font-medium">Real-time insights into your platform</p> <p className="text-slate-600 mt-2 font-medium">Track key indicators and member activity</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<StatsCard <StatsCard
title="Total Members" title="Total Members"
value={loading ? "..." : stats.totalUsers} value={loading ? "..." : stats.totalUsers}
@@ -90,17 +78,17 @@ export default function Home() {
<StatsCard <StatsCard
title="Active Athletes" title="Active Athletes"
value={loading ? "..." : stats.activeClients} value={loading ? "..." : stats.activeClients}
change="+8% this week" change="+5% vs last week"
trend="up" trend="up"
icon={Activity} icon={CalendarCheck}
color="green" color="emerald"
/> />
<StatsCard <StatsCard
title="Revenue" title="Revenue"
value={loading ? "..." : formatCurrency(stats.totalRevenue)} value={loading ? "..." : formatCurrency(stats.totalRevenue)}
change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`} change={`${stats.revenueGrowth > 0 ? "+" : ""}${stats.revenueGrowth}% growth`}
trend={stats.revenueGrowth >= 0 ? "up" : "down"} trend={stats.revenueGrowth >= 0 ? "up" : "down"}
icon={Zap} icon={CreditCard}
color="amber" color="amber"
/> />
<StatsCard <StatsCard
@@ -114,7 +102,7 @@ export default function Home() {
</div> </div>
</div> </div>
{/* Section: Member Management */} {/* Members Directory Section */}
<div className="mb-20"> <div className="mb-20">
<div className="mb-8"> <div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Members Directory</h2> <h2 className="text-3xl font-black text-slate-900">Members Directory</h2>
@@ -125,7 +113,7 @@ export default function Home() {
</div> </div>
</div> </div>
{/* Section: Key Insights */} {/* Key Insights Section */}
<div className="mb-20"> <div className="mb-20">
<div className="mb-8"> <div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Key Insights</h2> <h2 className="text-3xl font-black text-slate-900">Key Insights</h2>
@@ -135,31 +123,31 @@ export default function Home() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8"> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow"> <div className="p-6 bg-gradient-to-br from-blue-50 via-blue-100 to-blue-50 rounded-2xl border border-blue-200 hover:shadow-md transition-shadow">
<p className="text-xs text-blue-700 uppercase font-black tracking-wider">Members</p> <p className="text-xs text-blue-700 uppercase font-black tracking-wider">Active Subscriptions</p>
<p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p> <p className="text-3xl font-black text-blue-900 mt-4 truncate">{loading ? "..." : stats.totalUsers}</p>
<p className="text-sm text-blue-700 mt-2 font-semibold">Active subscriptions</p> <p className="text-sm text-blue-700 mt-2 font-semibold">Premium members active</p>
</div> </div>
</div> </div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8"> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow"> <div className="p-6 bg-gradient-to-br from-emerald-50 via-emerald-100 to-emerald-50 rounded-2xl border border-emerald-200 hover:shadow-md transition-shadow">
<p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Now</p> <p className="text-xs text-emerald-700 uppercase font-black tracking-wider">Training Activity</p>
<p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p> <p className="text-3xl font-black text-emerald-900 mt-4 truncate">{loading ? "..." : stats.activeClients}</p>
<p className="text-sm text-emerald-700 mt-2 font-semibold">Currently active</p> <p className="text-sm text-emerald-700 mt-2 font-semibold">Currently training</p>
</div> </div>
</div> </div>
<div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8"> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8">
<div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow"> <div className="p-6 bg-gradient-to-br from-amber-50 via-amber-100 to-amber-50 rounded-2xl border border-amber-200 hover:shadow-md transition-shadow">
<p className="text-xs text-amber-700 uppercase font-black tracking-wider">Revenue</p> <p className="text-xs text-amber-700 uppercase font-black tracking-wider">Total Revenue</p>
<p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p> <p className="text-2xl font-black text-amber-900 mt-4 truncate">{loading ? "..." : formatCurrency(stats.totalRevenue)}</p>
<p className="text-sm text-amber-700 mt-2 font-semibold">This period</p> <p className="text-sm text-amber-700 mt-2 font-semibold">This period earnings</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Section: Detailed Report - Full Width */} {/* Detailed Report Section */}
<div className="mb-20"> <div className="mb-20">
<div className="mb-8"> <div className="mb-8">
<h2 className="text-3xl font-black text-slate-900">Detailed Report</h2> <h2 className="text-3xl font-black text-slate-900">Detailed Report</h2>
@@ -172,6 +160,5 @@ export default function Home() {
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'
import { UserGrowthChart } from '@/components/charts/UserGrowthChart' import { UserGrowthChart } from '@/components/charts/UserGrowthChart'
import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart' import { MembershipDistributionChart } from '@/components/charts/MembershipDistributionChart'
import { RevenueChart } from '@/components/charts/RevenueChart' import { RevenueChart } from '@/components/charts/RevenueChart'
import { Card, CardHeader, CardContent } from '@/components/ui/card' import { Card, CardHeader, CardContent } from '@/components/ui/Card'
interface ChartData { interface ChartData {
label: string label: string
+20 -15
View File
@@ -65,15 +65,20 @@ export function Sidebar() {
]; ];
return ( return (
<aside className="w-64 bg-gradient-to-b from-white to-slate-50 h-screen fixed left-0 top-0 flex flex-col border-r border-slate-200/50"> <aside className="w-64 bg-slate-900 text-white h-screen fixed left-0 top-0 flex flex-col border-r border-slate-800">
<div className="p-6 border-b border-slate-200"> <div className="p-6 border-b border-slate-800">
<h1 className="text-2xl font-black bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> <div className="flex items-center gap-3 mb-2">
FitAI <div className="w-12 h-12">
<img src="/nextform-logo.png" alt="NextForm" className="w-full h-full object-contain" />
</div>
<h1 className="text-xl font-black bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
NextForm
</h1> </h1>
<p className="text-xs text-slate-600 font-medium mt-1">Sports Admin</p> </div>
<p className="text-xs text-slate-400">Fitness Admin</p>
</div> </div>
<nav className="flex-1 p-4 space-y-1"> <nav className="flex-1 p-4 space-y-2">
{menuItems.map((item) => { {menuItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = pathname === item.href; const isActive = pathname === item.href;
@@ -82,25 +87,25 @@ export function Sidebar() {
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-200 group font-medium ${isActive className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${isActive
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md shadow-blue-500/20" ? "bg-blue-600 text-white shadow-lg shadow-blue-900/20"
: "text-slate-700 hover:bg-slate-100 hover:text-blue-600"}`} : "text-slate-400 hover:bg-slate-800 hover:text-white"}`}
> >
<Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-blue-600"} /> <Icon size={20} className={isActive ? "text-white" : "text-slate-500 group-hover:text-white"} />
<span className="text-sm">{label}</span> <span className="font-medium">{label}</span>
</Link> </Link>
); );
})} })}
</nav> </nav>
<div className="p-4 border-t border-slate-200"> <div className="p-4 border-t border-slate-800">
<div className="flex items-center gap-3 px-4 py-3 rounded-2xl bg-slate-100"> <div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-slate-800/50">
<UserButton afterSignOutUrl="/" /> <UserButton afterSignOutUrl="/" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-slate-900 truncate"> <p className="text-sm font-medium text-white truncate">
{user?.fullName || "Admin User"} {user?.fullName || "Admin User"}
</p> </p>
<p className="text-xs text-slate-600 truncate"> <p className="text-xs text-slate-400 truncate">
{user?.primaryEmailAddress?.emailAddress} {user?.primaryEmailAddress?.emailAddress}
</p> </p>
</div> </div>
+28 -34
View File
@@ -6,67 +6,61 @@ interface StatsCardProps {
change?: string; change?: string;
trend?: "up" | "down" | "neutral"; trend?: "up" | "down" | "neutral";
icon: LucideIcon; icon: LucideIcon;
color?: "blue" | "green" | "amber" | "cyan"; color?: "blue" | "emerald" | "amber" | "cyan";
} }
export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) { export function StatsCard({ title, value, change, trend, icon: Icon, color = "blue" }: StatsCardProps) {
const colorStyles = { const colorStyles = {
blue: { blue: {
bg: "from-blue-50 via-blue-100 to-blue-50", bg: "from-blue-50 via-blue-100 to-blue-50",
text: "text-blue-950",
icon: "bg-gradient-to-br from-blue-500 to-blue-600",
border: "border-blue-200", border: "border-blue-200",
label: "text-blue-700" icon: "text-blue-700",
label: "text-blue-700",
value: "text-blue-900"
}, },
green: { emerald: {
bg: "from-emerald-50 via-emerald-100 to-emerald-50", bg: "from-emerald-50 via-emerald-100 to-emerald-50",
text: "text-emerald-950",
icon: "bg-gradient-to-br from-emerald-500 to-emerald-600",
border: "border-emerald-200", border: "border-emerald-200",
label: "text-emerald-700" icon: "text-emerald-700",
label: "text-emerald-700",
value: "text-emerald-900"
}, },
amber: { amber: {
bg: "from-amber-50 via-amber-100 to-amber-50", bg: "from-amber-50 via-amber-100 to-amber-50",
text: "text-amber-950",
icon: "bg-gradient-to-br from-amber-500 to-amber-600",
border: "border-amber-200", border: "border-amber-200",
label: "text-amber-700" icon: "text-amber-700",
label: "text-amber-700",
value: "text-amber-900"
}, },
cyan: { cyan: {
bg: "from-cyan-50 via-cyan-100 to-cyan-50", bg: "from-cyan-50 via-cyan-100 to-cyan-50",
text: "text-cyan-950",
icon: "bg-gradient-to-br from-cyan-500 to-cyan-600",
border: "border-cyan-200", border: "border-cyan-200",
label: "text-cyan-700" icon: "text-cyan-700",
}, label: "text-cyan-700",
value: "text-cyan-900"
}
}; };
const style = colorStyles[color]; const styles = colorStyles[color];
return ( return (
<div className={`bg-gradient-to-br ${style.bg} rounded-3xl border ${style.border} p-6 hover:shadow-xl transition-all duration-300 cursor-default group`}> <div className="bg-white rounded-3xl shadow-lg border border-slate-100 p-8 hover:shadow-xl transition-shadow">
<div className="flex items-start justify-between mb-4 gap-3"> <div className={`p-6 bg-gradient-to-br ${styles.bg} rounded-2xl border ${styles.border} hover:shadow-md transition-shadow`}>
<div className="flex-1 min-w-0"> <div className="flex items-start justify-between mb-4">
<p className={`text-xs ${style.label} uppercase font-black tracking-widest truncate`}> <div>
{title} <p className={`text-xs ${styles.label} uppercase font-black tracking-wider`}>{title}</p>
</p>
</div> </div>
<div className={`p-3 ${style.icon} rounded-2xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform flex-shrink-0`}> <div className={`${styles.icon} opacity-60`}>
<Icon size={20} className="text-white" /> <Icon size={24} />
</div> </div>
</div> </div>
<p className={`text-4xl font-black ${styles.value} mt-4 truncate`}>{value}</p>
<div className={`text-4xl font-black ${style.text} mb-3 truncate`}>
{value}
</div>
{change && ( {change && (
<div className="flex items-center gap-1 truncate"> <p className={`text-sm ${styles.label} mt-3 font-semibold`}>
<span className={`text-xs font-black uppercase tracking-wide ${style.label} truncate`}> {change}
{trend === "up" ? "↑" : trend === "down" ? "↓" : "→"} {change} </p>
</span>
</div>
)} )}
</div> </div>
</div>
); );
} }
+51 -18
View File
@@ -1,23 +1,56 @@
import React from 'react' import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { import { cn } from "@/lib/utils";
variant?: 'primary' | 'secondary'
children: React.ReactNode const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
} }
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors' ({ className, variant, size, asChild = false, ...props }, ref) => {
const variantClasses = { const Comp = asChild ? Slot : "button";
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
}
return ( return (
<button <Comp
className={`${baseClasses} ${variantClasses[variant]} ${className}`} className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} {...props}
> />
{children} );
</button> },
) );
} Button.displayName = "Button";
export { Button, buttonVariants };
+71 -33
View File
@@ -1,38 +1,76 @@
import React from 'react' import * as React from "react"
interface CardProps { import { cn } from "@/lib/utils"
children: React.ReactNode
className?: string
}
export function Card({ children, className = '' }: CardProps) { const Card = React.forwardRef<
return ( HTMLDivElement,
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</div> <div
) ref={ref}
} className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
export function CardHeader({ children, className = '' }: CardProps) { const CardHeader = React.forwardRef<
return ( HTMLDivElement,
<div className={`mb-4 ${className}`}> React.HTMLAttributes<HTMLDivElement>
{children} >(({ className, ...props }, ref) => (
</div> <div
) ref={ref}
} className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
export function CardContent({ children, className = '' }: CardProps) { const CardTitle = React.forwardRef<
return ( HTMLParagraphElement,
<div className={className}> React.HTMLAttributes<HTMLHeadingElement>
{children} >(({ className, ...props }, ref) => (
</div> <h3
) ref={ref}
} className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
export function CardTitle({ children, className = '' }: CardProps) { const CardDescription = React.forwardRef<
return ( HTMLParagraphElement,
<div className={`text-sm font-medium ${className}`}> React.HTMLAttributes<HTMLParagraphElement>
{children} >(({ className, ...props }, ref) => (
</div> <p
) ref={ref}
} className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
interface Recommendation { interface Recommendation {
id: string; id: string;
+153 -244
View File
@@ -2,8 +2,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { UserGrid } from "@/components/users/UserGrid"; import { UserGrid } from "@/components/users/UserGrid";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/Button";
import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardContent } from "@/components/ui/Card";
interface User { interface User {
id: string; id: string;
@@ -34,7 +34,6 @@ export function UserManagement() {
const [selectedUser, setSelectedUser] = useState<User | null>(null); const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [activeTab, setActiveTab] = useState<"users" | "details">("users");
const [editForm, setEditForm] = useState<{ const [editForm, setEditForm] = useState<{
firstName: string; firstName: string;
lastName: string; lastName: string;
@@ -206,83 +205,25 @@ export function UserManagement() {
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-6">
{/* Header with Count */} <div className="flex justify-between items-center">
<div className="flex items-center justify-between"> <h2 className="text-2xl font-bold">User Management</h2>
<h3 className="text-lg font-black text-slate-900"> <div className="flex gap-2">
{users.length} {users.length === 1 ? 'user' : 'users'} <Button
</h3> variant={filter === "all" ? "primary" : "secondary"}
{selectedUser && (
<p className="text-sm text-blue-600 font-semibold">
Selected: {selectedUser.firstName} {selectedUser.lastName}
</p>
)}
</div>
{/* Compact Controls - Always Visible but Organized */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4 bg-gradient-to-r from-slate-50 to-blue-50 rounded-lg border border-slate-200">
{/* Filter Row */}
<div>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Roles</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setFilter("all")} onClick={() => setFilter("all")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "all"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
> >
All All Users
</button> </Button>
<button <Button
onClick={() => setFilter("client")} variant="secondary"
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${ onClick={() => selectedUser && handleEditUser(selectedUser)}
filter === "client" disabled={!selectedUser}
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
> >
Clients Edit User
</button> </Button>
<button <Button
onClick={() => setFilter("trainer")} variant="secondary"
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "trainer"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Trainers
</button>
<button
onClick={() => setFilter("admin")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "admin"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Admins
</button>
<button
onClick={() => setFilter("superAdmin")}
className={`px-2 py-1 text-xs font-semibold rounded-lg transition-all ${
filter === "superAdmin"
? "bg-blue-500 text-white shadow-md"
: "bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"
}`}
>
Super Admins
</button>
</div>
</div>
{/* Action Buttons Row */}
<div>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mb-2">Quick Actions</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => { onClick={() => {
setEditForm({ setEditForm({
firstName: "", firstName: "",
@@ -294,69 +235,67 @@ export function UserManagement() {
setSelectedUser(null); setSelectedUser(null);
setIsEditing(true); setIsEditing(true);
}} }}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-blue-500 text-white hover:bg-blue-600"
> >
+ Invite Invite User
</button> </Button>
<button <Button
onClick={() => selectedUser && handleEditUser(selectedUser)} variant="secondary"
disabled={!selectedUser}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
>
Edit
</button>
<button
onClick={() => selectedUser && handleDeleteUser(selectedUser)} onClick={() => selectedUser && handleDeleteUser(selectedUser)}
disabled={!selectedUser} disabled={!selectedUser}
className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200 disabled:opacity-50"
> >
Delete Delete User
</button> </Button>
<button onClick={handleRefresh} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"> <Button
variant={filter === "client" ? "primary" : "secondary"}
onClick={() => setFilter("client")}
>
Clients
</Button>
<Button
variant={filter === "trainer" ? "primary" : "secondary"}
onClick={() => setFilter("trainer")}
>
Trainers
</Button>
<Button
variant={filter === "admin" ? "primary" : "secondary"}
onClick={() => setFilter("admin")}
>
Admins
</Button>
<Button
variant={filter === "superAdmin" ? "primary" : "secondary"}
onClick={() => setFilter("superAdmin")}
>
Super Admins
</Button>
</div>
</div>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-600">
Showing {users.length} users
{selectedUser && (
<span className="ml-4 text-blue-600">
Selected: {selectedUser.firstName} {selectedUser.lastName}
</span>
)}
</div>
<div className="flex gap-2">
<Button variant="secondary" onClick={handleRefresh}>
Refresh Refresh
</button> </Button>
<button onClick={handleExport} className="px-2 py-1 text-xs font-semibold rounded-lg bg-white text-slate-700 hover:bg-slate-100 border border-slate-200"> <Button variant="secondary" onClick={handleExport}>
Export Export CSV
</button> </Button>
</div>
</div> </div>
</div> </div>
{/* Tab Navigation */} <Card>
<div className="flex gap-2 border-b border-slate-200">
<button
onClick={() => setActiveTab("users")}
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
activeTab === "users"
? "border-blue-500 text-blue-600"
: "border-transparent text-slate-600 hover:text-slate-900"
}`}
>
User List ({users.length})
</button>
<button
onClick={() => setActiveTab("details")}
disabled={!selectedUser}
className={`px-4 py-3 text-sm font-semibold border-b-2 transition-all ${
activeTab === "details"
? "border-blue-500 text-blue-600"
: "border-transparent text-slate-600 hover:text-slate-900"
} ${!selectedUser ? "opacity-50 cursor-not-allowed" : ""}`}
>
User Details
</button>
</div>
{/* Tab Content */}
{activeTab === "users" && (
<Card className="border border-slate-200">
<CardContent className="p-0"> <CardContent className="p-0">
<UserGrid <UserGrid
users={users} users={users}
onUserSelect={(user) => { onUserSelect={(user) => handleUserSelect(user)}
handleUserSelect(user);
setActiveTab("details");
}}
onEditUser={handleEditUser} onEditUser={handleEditUser}
onDeleteUser={handleDeleteUser} onDeleteUser={handleDeleteUser}
onBulkDelete={handleBulkDelete} onBulkDelete={handleBulkDelete}
@@ -364,70 +303,6 @@ export function UserManagement() {
/> />
</CardContent> </CardContent>
</Card> </Card>
)}
{activeTab === "details" && selectedUser && (
<div className="p-6 bg-white rounded-xl border border-slate-200">
<div className="mb-6">
<h3 className="text-xl font-black text-slate-900">
{selectedUser.firstName} {selectedUser.lastName}
</h3>
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p>
</div>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Joined</p>
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p>
</div>
{selectedUser.client && (
<>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p>
</div>
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Status</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
</div>
</>
)}
<div className="space-y-2">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
</div>
</div>
<div className="flex gap-3 mt-8 pt-6 border-t border-slate-200">
<button
onClick={() => selectedUser && handleEditUser(selectedUser)}
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all"
>
Edit User
</button>
<button
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all"
>
Delete User
</button>
<a
href={`/users/${selectedUser.id}`}
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all"
>
Full Profile
</a>
</div>
</div>
)}
{isEditing && editForm && ( {isEditing && editForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@@ -563,65 +438,99 @@ export function UserManagement() {
)} )}
{selectedUser && ( {selectedUser && (
<div className="mt-8 pt-8 border-t border-slate-200"> <Card>
<div className="mb-6"> <CardHeader className="flex flex-row items-center justify-between">
<h3 className="text-lg font-black text-slate-900"> <h3 className="text-lg font-semibold">User Details</h3>
<a
href={`/users/${selectedUser.id}`}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
>
View Full Profile & Recommendations
</a>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-medium mb-2">Basic Information</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Name:</span>{" "}
{selectedUser.firstName} {selectedUser.lastName} {selectedUser.firstName} {selectedUser.lastName}
</h3> </p>
<p className="text-sm text-slate-600 mt-1">{selectedUser.email}</p> <p>
</div> <span className="font-medium">Email:</span>{" "}
{selectedUser.email}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> </p>
<div className="space-y-3"> <p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Role</p> <span className="font-medium">Phone:</span>{" "}
<p className="text-base font-semibold text-slate-900">{selectedUser.role}</p> {selectedUser.phone || "N/A"}
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Joined</p> </p>
<p className="text-base font-semibold text-slate-900">{new Date(selectedUser.createdAt).toLocaleDateString()}</p> <p>
</div> <span className="font-medium">Role:</span>{" "}
{selectedUser.role}
{selectedUser.client && ( </p>
<div className="space-y-3"> <p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Membership</p> <span className="font-medium">Joined:</span>{" "}
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipType}</p> {new Date(selectedUser.createdAt).toLocaleDateString()}
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Status</p>
<p className="text-base font-semibold text-slate-900">{selectedUser.client.membershipStatus}</p>
</div>
)}
<div className="space-y-3">
<p className="text-xs font-black uppercase tracking-wider text-slate-600">Check-ins This Month</p>
<p className="text-2xl font-black text-blue-600">{selectedUser.checkInsThisMonth || 0}</p>
<p className="text-xs font-black uppercase tracking-wider text-slate-600 mt-4">Last Check-in</p>
<p className="text-sm text-slate-900">
{selectedUser.lastCheckInTime
? new Date(selectedUser.lastCheckInTime).toLocaleDateString()
: "Never"}
</p> </p>
</div> </div>
</div> </div>
<div className="flex gap-3 mt-6"> {selectedUser.client && (
<button <div>
onClick={() => selectedUser && handleEditUser(selectedUser)} <h4 className="font-medium mb-2">Client Information</h4>
className="px-5 py-2 bg-blue-50 text-blue-600 rounded-lg font-semibold text-sm hover:bg-blue-100 transition-all" <div className="space-y-1 text-sm">
> <p>
Edit <span className="font-medium">Membership:</span>{" "}
</button> {selectedUser.client.membershipType}
<button </p>
onClick={() => selectedUser && handleDeleteUser(selectedUser)} <p>
className="px-5 py-2 bg-red-50 text-red-600 rounded-lg font-semibold text-sm hover:bg-red-100 transition-all" <span className="font-medium">Status:</span>{" "}
> {selectedUser.client.membershipStatus}
Delete </p>
</button> <p>
<a <span className="font-medium">Member Since:</span>{" "}
href={`/users/${selectedUser.id}`} {new Date(
className="px-5 py-2 bg-gradient-to-r from-blue-500 to-cyan-500 text-white rounded-lg font-semibold text-sm hover:shadow-lg transition-all" selectedUser.client.joinDate,
> ).toLocaleDateString()}
Full Profile </p>
</a> <p>
<span className="font-medium">Last Visit:</span>{" "}
{selectedUser.client.lastVisit
? new Date(
selectedUser.client.lastVisit,
).toLocaleDateString()
: "Never"}
</p>
</div> </div>
</div> </div>
)} )}
<div>
<h4 className="font-medium mb-2">Check-In Statistics</h4>
<div className="space-y-1 text-sm">
<p>
<span className="font-medium">Last Check-In:</span>{" "}
{selectedUser.lastCheckInTime
? new Date(
selectedUser.lastCheckInTime,
).toLocaleString()
: "Never"}
</p>
<p>
<span className="font-medium">This Week:</span>{" "}
{selectedUser.checkInsThisWeek || 0} check-ins
</p>
<p>
<span className="font-medium">This Month:</span>{" "}
{selectedUser.checkInsThisMonth || 0} check-ins
</p>
</div>
</div>
</div>
</CardContent>
</Card>
)}
</div> </div>
); );
} }
+12 -171
View File
@@ -1,174 +1,15 @@
# FitAI ## fitai
Integrated AI solution for fitness houses and their clients with Clerk authentication. # description
## Project Structure - fitai is integrated ai solution for fitness houses and their clients,
its allow to easy menagment of clients, tracking of payments, usage of resourcess,
attendance, habits etc.
these will be phase one:
solution is composed of a admin app, where we are doing managment tasks, we visualize and
expose importatnt data to menagment and trainers, and a expo/reactnative mobile app for users.
via app we will be tracking attendance and payments, we will be sending notification etc.
``` # phase 2
fitai/ we will be tracking user inputs via manual input and devices, backend will analyze data and propose
├── apps/ excercises etc.
│ ├── admin/ # Next.js admin dashboard
│ └── mobile/ # React Native mobile app (Expo)
├── packages/
│ └── shared/ # Shared types and utilities
└── AGENTS.md # Development guidelines
```
## Getting Started
### Prerequisites
- Node.js >= 18.0.0
- npm >= 9.0.0
- Clerk account (sign up at https://clerk.com)
### Installation
```bash
# Install root dependencies
npm install
# Install admin dependencies
cd apps/admin && npm install
# Install mobile dependencies
cd apps/mobile && npm install --legacy-peer-deps
```
### Authentication Setup
FitAI uses Clerk for authentication. Follow these steps:
1. **Create a Clerk account** at https://dashboard.clerk.com
2. **Create a new application** in the Clerk dashboard
3. **Copy your API keys** (Publishable Key and Secret Key)
4. **Configure environment variables**:
**Admin App** (`apps/admin/.env.local`):
```env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_key_here
```
**Mobile App** (`apps/mobile/.env`):
```env
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
```
📖 **See [CLERK_SETUP.md](./CLERK_SETUP.md) for detailed setup instructions**
### Development
**Important**: Set up environment variables before running the apps!
```bash
# Admin dashboard (http://localhost:3000)
cd apps/admin && npm run dev
# Mobile app (http://localhost:8081) - Requires Expo SDK 54
cd apps/mobile && npm start
```
**First-time setup checklist**:
- [ ] Create Clerk account and application
- [ ] Add API keys to `.env.local` (admin) and `.env` (mobile)
- [ ] Verify both apps start without errors
- [ ] Test sign-up and sign-in flows
### Mobile App Setup
- **Expo SDK**: 50 (stable, compatible with Expo Go)
- **Assets**: Placeholder icons and splash screen included
- **Navigation**: Expo Router with tab-based layout
- **Authentication**: Secure storage with expo-secure-store
- **Babel**: babel-preset-expo for proper transpilation
### Known Compatibility Notes
- Use Expo Go with SDK 50 for mobile testing
- For SDK 54, upgrade all dependencies to latest versions
- Current setup prioritizes stability over latest features
### Build & Test
```bash
# Build all apps
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Type checking
npm run typecheck
```
## Features
### Authentication (Clerk)
- 🔐 Secure email/password authentication
- ✉️ Email verification
- 🔄 Session management
- 🎨 Customizable UI components
- 📱 Multi-platform support (Web + Mobile)
- 🛡️ Built-in security features
### Admin Dashboard
- 👥 User management (CRUD operations)
- 📊 Analytics dashboard with charts
- 🎯 Role-based access control
- 📈 Data visualization with AG Grid
- 💳 Payment tracking (coming soon)
- 📅 Attendance monitoring (coming soon)
### Mobile App
- 🔐 Secure sign-in/sign-up
- 👤 User profile management
- 📱 Native mobile experience
- 🔔 Push notifications ready
- ✅ Attendance check-in (coming soon)
- 💰 Payment history (coming soon)
## Tech Stack
### Authentication
- **Clerk**: Complete authentication and user management platform
### Frontend
- **Admin**: Next.js 14 (App Router), React 19, TypeScript, Tailwind CSS
- **Mobile**: React Native, Expo SDK 54, Expo Router, TypeScript
### Backend & Database
- **Database**: SQLite with Drizzle ORM
- **API**: Next.js API Routes (REST)
### Development Tools
- **State Management**: React Query, React Hook Form
- **Validation**: Zod schemas
- **Data Grid**: AG Grid for advanced user management
- **Charts**: AG Charts for analytics and visualization
- **Testing**: Jest, Testing Library (configured)
## Project Structure
```
fitai/
├── apps/
│ ├── admin/ # Next.js admin dashboard
│ │ ├── src/
│ │ │ ├── app/ # App Router pages & API routes
│ │ │ ├── components/
│ │ │ └── lib/ # Database & utilities
│ │ └── .env.local # Admin environment variables
│ │
│ └── mobile/ # Expo React Native app
│ ├── src/
│ │ ├── app/ # Expo Router screens
│ │ │ ├── (auth)/ # Authentication screens
│ │ │ └── (tabs)/ # Main app tabs
│ │ └── components/
│ └── .env # Mobile environment variables
│
├── packages/
│ ├── database/ # Drizzle ORM schemas & DB client
│ └── shared/ # Shared types & utilities
│
└── CLERK_SETUP.md # Detailed authentication setup guide
```
-16
View File
@@ -1,16 +0,0 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
f ^F ^V SPACE * Forward one window (or _N lines).