feat: Next.js 14 web admin scaffold — auth, sidebar, dashboard, placeholders

This commit is contained in:
Forge
2026-03-25 08:23:25 +08:00
parent dc01eac449
commit 310a9453de
37 changed files with 2755 additions and 292 deletions

View File

@@ -0,0 +1,71 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search, UserPlus } from 'lucide-react';
export default function ClientsPage() {
const [search, setSearch] = useState('');
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Clients</h1>
<p className="text-slate-500 text-sm mt-1">Manage your ISP subscribers</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<UserPlus size={16} className="mr-2" />
Add Client
</Button>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input
placeholder="Search clients..."
className="pl-9"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No clients yet. Add your first client to get started.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,100 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Users, Wifi, DollarSign, AlertTriangle } from 'lucide-react';
interface DashboardSummary {
totalClients: number;
activeSubscribers: number;
todayCollections: number;
overdueInvoices: number;
}
const kpiCards = [
{
key: 'totalClients' as const,
label: 'Total Clients',
icon: Users,
color: '#0891B2',
format: (v: number) => v.toLocaleString(),
},
{
key: 'activeSubscribers' as const,
label: 'Active Subscribers',
icon: Wifi,
color: '#059669',
format: (v: number) => v.toLocaleString(),
},
{
key: 'todayCollections' as const,
label: "Today's Collections",
icon: DollarSign,
color: '#7C3AED',
format: (v: number) => '₱' + v.toLocaleString('en-PH', { minimumFractionDigits: 2 }),
},
{
key: 'overdueInvoices' as const,
label: 'Overdue Invoices',
icon: AlertTriangle,
color: '#DC2626',
format: (v: number) => v.toLocaleString(),
},
];
export default function DashboardPage() {
const { data, isLoading, error } = useQuery<DashboardSummary>({
queryKey: ['dashboard', 'summary'],
queryFn: async () => {
const res = await api.get('/api/v1/dashboard/summary');
return res.data;
},
});
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
<p className="text-slate-500 text-sm mt-1">Overview of your ISP operations</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
Failed to load dashboard data. The API endpoint may not be available yet.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{kpiCards.map((card) => {
const Icon = card.icon;
return (
<Card key={card.key} className="border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-slate-500">
{card.label}
</CardTitle>
<div
className="w-9 h-9 rounded-lg flex items-center justify-center"
style={{ backgroundColor: card.color + '1A' }}
>
<Icon size={18} style={{ color: card.color }} />
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<Skeleton className="h-8 w-24" />
) : (
<p className="text-2xl font-bold text-slate-800">
{data ? card.format(data[card.key]) : '—'}
</p>
)}
</CardContent>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
export default function InvoicesPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Invoices</h1>
<p className="text-slate-500 text-sm mt-1">Track and manage billing invoices</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search invoices..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No invoices found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

32
app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,32 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import Sidebar from '@/components/layout/sidebar';
import Topbar from '@/components/layout/topbar';
export default function AppLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const accessToken = useAuthStore((s) => s.accessToken);
useEffect(() => {
if (!accessToken) {
router.replace('/login');
}
}, [accessToken, router]);
if (!accessToken) {
return null;
}
return (
<div className="flex min-h-screen" style={{ backgroundColor: '#F8FAFC' }}>
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Topbar />
<main className="flex-1 overflow-y-auto p-6">{children}</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
export default function PaymentsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Payments</h1>
<p className="text-slate-500 text-sm mt-1">Record and review payment transactions</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search payments..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Ref #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Channel</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No payments recorded yet.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
export default function RemittancesPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Remittances</h1>
<p className="text-slate-500 text-sm mt-1">Collector cash remittance management</p>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4" />
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Reference</TableHead>
<TableHead>Collector</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center py-12 text-slate-400">
No remittances found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,33 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart3 } from 'lucide-react';
export default function ReportsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Reports</h1>
<p className="text-slate-500 text-sm mt-1">Financial and operational reports</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{['Collections Report', 'Subscriber Growth', 'Invoice Aging', 'Technician Performance'].map(
(report) => (
<Card key={report} className="border shadow-sm hover:shadow-md transition-shadow cursor-pointer">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2 text-slate-700">
<BarChart3 size={18} style={{ color: '#0891B2' }} />
{report}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-400 text-sm">Coming soon report generation will be available here.</p>
</CardContent>
</Card>
)
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Plus, MapPin } from 'lucide-react';
export default function AreasPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Areas & Zones</h1>
<p className="text-slate-500 text-sm mt-1">Define service coverage areas</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
Add Area
</Button>
</div>
<Card className="border shadow-sm">
<CardContent className="flex flex-col items-center justify-center py-16 text-slate-400">
<MapPin size={32} className="mb-3 text-slate-300" />
<p className="text-sm font-medium">No areas defined yet</p>
<p className="text-xs mt-1">Create areas to group clients by coverage zone</p>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,31 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
export default function PlansPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Service Plans</h1>
<p className="text-slate-500 text-sm mt-1">Manage internet subscription plans</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
Add Plan
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="border shadow-sm border-dashed border-slate-300">
<CardContent className="flex flex-col items-center justify-center py-12 text-slate-400">
<Plus size={24} className="mb-2" />
<p className="text-sm">Create your first plan</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
'use client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
export default function TenantSettingsPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-800">Tenant Settings</h1>
<p className="text-slate-500 text-sm mt-1">Configure your ISP business details</p>
</div>
<Card className="border shadow-sm max-w-2xl">
<CardHeader>
<CardTitle className="text-base">Business Information</CardTitle>
<CardDescription>Update your ISP company details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label>Business Name</Label>
<Input placeholder="e.g. My Internet Services" />
</div>
<div className="space-y-1.5">
<Label>Address</Label>
<Input placeholder="Business address" />
</div>
<div className="space-y-1.5">
<Label>Phone</Label>
<Input placeholder="+63 9XX XXX XXXX" />
</div>
<div className="space-y-1.5">
<Label>Email</Label>
<Input type="email" placeholder="info@yourisp.com" />
</div>
<Button style={{ backgroundColor: '#0891B2' }}>Save Changes</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,53 @@
'use client';
import { Card, CardContent } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { UserPlus } from 'lucide-react';
export default function UsersPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Users</h1>
<p className="text-slate-500 text-sm mt-1">Manage staff accounts and permissions</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<UserPlus size={16} className="mr-2" />
Invite User
</Button>
</div>
<Card className="border shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={5} className="text-center py-12 text-slate-400">
No users found. Invite your team members.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,62 @@
'use client';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Search, Plus } from 'lucide-react';
export default function TicketsPage() {
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-800">Tickets</h1>
<p className="text-slate-500 text-sm mt-1">Support and installation tickets</p>
</div>
<Button style={{ backgroundColor: '#0891B2' }}>
<Plus size={16} className="mr-2" />
New Ticket
</Button>
</div>
<Card className="border shadow-sm">
<CardHeader className="pb-4">
<div className="relative max-w-sm">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<Input placeholder="Search tickets..." className="pl-9" />
</div>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Ticket #</TableHead>
<TableHead>Client</TableHead>
<TableHead>Type</TableHead>
<TableHead>Priority</TableHead>
<TableHead>Status</TableHead>
<TableHead>Assigned To</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell colSpan={7} className="text-center py-12 text-slate-400">
No tickets found.
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

138
app/(auth)/login/page.tsx Normal file
View File

@@ -0,0 +1,138 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { toast } from 'sonner';
import { api } from '@/lib/api';
import { useAuthStore } from '@/lib/auth-store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2 } from 'lucide-react';
const loginSchema = z.object({
tenantSlug: z.string().min(1, 'Tenant slug is required'),
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
type LoginForm = z.infer<typeof loginSchema>;
export default function LoginPage() {
const router = useRouter();
const setAuth = useAuthStore((s) => s.setAuth);
const [loading, setLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginForm) => {
setLoading(true);
try {
const res = await api.post('/api/v1/auth/login', data);
const { accessToken, user } = res.data;
setAuth({ accessToken, tenantSlug: data.tenantSlug, user });
toast.success('Welcome back, ' + user.firstName + '!');
router.push('/dashboard');
} catch (err: unknown) {
const error = err as { response?: { data?: { message?: string } } };
toast.error(error.response?.data?.message || 'Login failed. Check your credentials.');
} finally {
setLoading(false);
}
};
return (
<div
className="min-h-screen flex items-center justify-center p-4"
style={{ backgroundColor: '#F8FAFC' }}
>
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white font-bold text-xl mx-auto mb-3"
style={{ backgroundColor: '#0891B2' }}
>
F
</div>
<h1 className="text-2xl font-bold text-slate-800">FiberOps</h1>
<p className="text-slate-500 text-sm mt-1">ISP Management Platform</p>
</div>
<Card className="shadow-sm">
<CardHeader>
<CardTitle className="text-lg">Sign in to your account</CardTitle>
<CardDescription>Enter your tenant and credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="tenantSlug">Tenant Slug</Label>
<Input
id="tenantSlug"
placeholder="e.g. demo-isp"
{...register('tenantSlug')}
/>
{errors.tenantSlug && (
<p className="text-xs text-red-500">{errors.tenantSlug.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="admin@yourisp.com"
{...register('email')}
/>
{errors.email && (
<p className="text-xs text-red-500">{errors.email.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
{...register('password')}
/>
{errors.password && (
<p className="text-xs text-red-500">{errors.password.message}</p>
)}
</div>
<Button
type="submit"
className="w-full"
disabled={loading}
style={{ backgroundColor: '#0891B2' }}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing in
</>
) : (
'Sign In'
)}
</Button>
</form>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,94 +1,64 @@
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
.theme {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

View File

@@ -1,38 +1,24 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { Geist } from "next/font/google";
import { cn } from "@/lib/utils";
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import Providers from '@/components/providers';
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'FiberOps Admin',
description: 'ISP Management Platform',
};
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en" className={cn("font-sans", geist.variable)}>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);

View File

@@ -1,101 +1,5 @@
import Image from "next/image";
import { redirect } from 'next/navigation';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="https://nextjs.org/icons/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="https://nextjs.org/icons/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
redirect('/dashboard');
}