mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Initial
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { Tabs, TabsList, TabsTrigger, } from "@/components/custom/original-tabs";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AdminSideTabs() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const path = location.pathname;
|
||||
|
||||
const tabItems = [
|
||||
{ value: "tab-1", label: "Dashboard", paths: ["/admin"] },
|
||||
{ value: "tab-2", label: "User Management", paths: ["users"] },
|
||||
{ value: "tab-3", label: "Content Management", paths: ["content", "courses"] },
|
||||
{ value: "tab-5", label: "Site Content", paths: ["contents"] },
|
||||
]
|
||||
|
||||
const getTabValue = () => {
|
||||
// ← exact match for /admin only
|
||||
const isDashboard = path === '/admin'
|
||||
if (isDashboard) return "tab-1"
|
||||
|
||||
const found = tabItems.slice(1).find(item =>
|
||||
item.paths.some(p => p && path.includes(p))
|
||||
)
|
||||
return found ? found.value : ""
|
||||
}
|
||||
|
||||
const [value, setValue] = useState(getTabValue());
|
||||
|
||||
const handleTabChange = (val) => {
|
||||
setValue(val)
|
||||
const tab = tabItems.find(t => t.value === val)
|
||||
const primaryPath = tab?.paths?.[0]
|
||||
|
||||
// ← no adminId, just /admin/users etc.
|
||||
const targetPath = primaryPath && primaryPath !== '/admin'
|
||||
? `/admin/${primaryPath}`
|
||||
: `/admin`
|
||||
|
||||
navigate(targetPath)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setValue(getTabValue());
|
||||
}, [path]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Tabs value={value} onValueChange={handleTabChange}>
|
||||
<ScrollArea className="max-w-full overflow-x-auto border-b w-full">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"bg-background rounded-none justify-start mx-2 my-1 flex gap-1"
|
||||
)}
|
||||
>
|
||||
{tabItems.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="data-[state=active]:bg-muted data-[state=active]:shadow-none hover:bg-muted text-muted-foreground data-[state=active]:text-foreground"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: AdminAppLayout.jsx
|
||||
* Type of Program: Layout Route
|
||||
* Description: A Layout designated for Admin End-User.
|
||||
* Module: Admin
|
||||
* Author: lash0000
|
||||
* Date Created: November. 28, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG NUMBER DESCRIPTION
|
||||
* Oct. 10, 2025 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
***********************************************************************************************************************************************************************/
|
||||
import { Outlet, useNavigate } from "react-router-dom"
|
||||
import { useAuth } from "@/contexts/AuthContext"
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Toaster } from "sonner"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import AdminSideTabs from "../components/AdminSideTabs"
|
||||
import UserMenu from "@/components/generic/UserMenu"
|
||||
import { ROLE_CONFIG } from "@/data/profile.data"
|
||||
|
||||
const AdminLayout = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const role = ROLE_CONFIG[user?.acc_type] ?? { label: user?.acc_type, variant: 'outline' }
|
||||
|
||||
async function handleLogout() {
|
||||
// setSignOutOpen(true);
|
||||
// setSignOutLoading(true);
|
||||
|
||||
// try {
|
||||
// await logout();
|
||||
// navigate("/", { replace: true });
|
||||
// } finally {
|
||||
// setSignOutLoading(false);
|
||||
// setSignOutOpen(false);
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="philproperties-admin">
|
||||
<TooltipProvider>
|
||||
<div className={cn('')} >
|
||||
<div className="w-full flex items-center justify-between xs:px-4 lg:px-5 py-3">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="xs:hidden sm:block w-40 cursor-pointer" onClick={() => navigate(`/admin/${id}`)}>
|
||||
<img src="/philpro-white.png" alt="" className="object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<svg
|
||||
data-testid="geist-icon"
|
||||
height="16"
|
||||
width="16"
|
||||
viewBox="0 0 16 16"
|
||||
strokeLinejoin="round"
|
||||
className="xs:hidden sm:block fill-slate-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="name" className="lg:-ml-1 font-medium text-sm flex items-center gap-2">
|
||||
<Badge variant={role.variant} className="xs:hidden md:block capitalize">
|
||||
{role.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
<div id="side-tabs">
|
||||
<AdminSideTabs />
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-body">
|
||||
<Outlet />
|
||||
<Toaster position="bottom-right" richColors />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminLayout
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Admin = () => {
|
||||
return (
|
||||
<div>
|
||||
Admin Page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Admin
|
||||
@@ -0,0 +1,31 @@
|
||||
import ProtectedRoute from '../../../routes/ProtectedRoute'
|
||||
import AdminLayout from '../layouts/AdminLayout'
|
||||
import Admin from '../pages/Admin'
|
||||
import ProfilePage from '@/components/generic/Profile'
|
||||
|
||||
export const AdminRoutes = {
|
||||
element: <ProtectedRoute allowedRoles={['admin']} />,
|
||||
children: [
|
||||
{
|
||||
path: '/admin',
|
||||
element: <AdminLayout />, // ← shared sidebar/header for all admin pages
|
||||
children: [
|
||||
// Admin Management
|
||||
{ index: true, element: <Admin /> }, // /admin
|
||||
{ path: 'my-profile', element: <ProfilePage /> },
|
||||
// Users Management
|
||||
// {
|
||||
// path: 'users',
|
||||
// element: <UsersLayout />, // ← shared header/nav for user pages
|
||||
// children: [
|
||||
// { index: true, element: <Users /> }, // /admin/users
|
||||
// { path: 'add', element: <UsersAdd /> }, // /admin/users/add
|
||||
// { path: 'edit/:id', element: <UsersEdit /> }, // /admin/users/edit/123
|
||||
// ]
|
||||
// },
|
||||
|
||||
// Add here
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: login-form.jsx
|
||||
* Type of Program: Frontend Layout
|
||||
* Description: Frontend layout Login Page.
|
||||
* Module: User Credentials
|
||||
* Author: lash0000
|
||||
* Date Created: Oct. 10, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG NUMBER DESCRIPTION
|
||||
* Oct. 10, 2025 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
***********************************************************************************************************************************************************************/
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSeparator, } from '@/components/ui/field'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
||||
import { Eye, EyeOff, LoaderCircle } from 'lucide-react'
|
||||
|
||||
// ─── Schema ──────────────────────────────────────────────────────────────────
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(1, 'Password is required'),
|
||||
})
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
export function LoginForm({ className, ...props }) {
|
||||
const { login } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false)
|
||||
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: { email: '', password: '' },
|
||||
})
|
||||
|
||||
const onSubmit = async ({ email, password }) => {
|
||||
const result = await login({ email, password })
|
||||
|
||||
if (result.success) {
|
||||
switch (result.user.acc_type) {
|
||||
case 'admin': navigate('/admin'); break
|
||||
case 'staff': navigate('/staff'); break
|
||||
case 'client': navigate('/client'); break
|
||||
default: navigate('/login')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setErrorMessage(result.message || 'Invalid credentials')
|
||||
setErrorDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleGoogle = () => {
|
||||
window.location.href = '/api/auth/google'
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className={cn('flex flex-col gap-6', className)}
|
||||
{...props}
|
||||
>
|
||||
<FieldGroup>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<h1 className="text-2xl font-bold tracking-tighter">
|
||||
Take the next step towards new learnings.
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm text-balance">
|
||||
Learn and grow for your career.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Button variant="outline" type="button" onClick={handleGoogle}>
|
||||
<svg viewBox="0 0 128 128" >
|
||||
<path fill="#fff" d="M44.59 4.21a63.28 63.28 0 004.33 120.9 67.6 67.6 0 0032.36.35 57.13 57.13 0 0025.9-13.46 57.44 57.44 0 0016-26.26 74.33 74.33 0 001.61-33.58H65.27v24.69h34.47a29.72 29.72 0 01-12.66 19.52 36.16 36.16 0 01-13.93 5.5 41.29 41.29 0 01-15.1 0A37.16 37.16 0 0144 95.74a39.3 39.3 0 01-14.5-19.42 38.31 38.31 0 010-24.63 39.25 39.25 0 019.18-14.91A37.17 37.17 0 0176.13 27a34.28 34.28 0 0113.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.22 61.22 0 0087.2 4.59a64 64 0 00-42.61-.38z" /><path fill="#e33629" d="M44.59 4.21a64 64 0 0142.61.37 61.22 61.22 0 0120.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.28 34.28 0 00-13.64-8 37.17 37.17 0 00-37.46 9.74 39.25 39.25 0 00-9.18 14.91L8.76 35.6A63.53 63.53 0 0144.59 4.21z" /><path fill="#f8bd00" d="M3.26 51.5a62.93 62.93 0 015.5-15.9l20.73 16.09a38.31 38.31 0 000 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 01-5.5-40.9z" /><path fill="#587dbd" d="M65.27 52.15h59.52a74.33 74.33 0 01-1.61 33.58 57.44 57.44 0 01-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0012.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68z" /><path fill="#319f43" d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0044 95.74a37.16 37.16 0 0014.08 6.08 41.29 41.29 0 0015.1 0 36.16 36.16 0 0013.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 01-25.9 13.47 67.6 67.6 0 01-32.36-.35 63 63 0 01-23-11.59A63.73 63.73 0 018.75 92.4z" />
|
||||
</svg>
|
||||
Login with Google
|
||||
</Button>
|
||||
</Field>
|
||||
|
||||
<FieldSeparator className="flex items-center my-1.5 h-px">
|
||||
Or continue with
|
||||
</FieldSeparator>
|
||||
|
||||
{/* Email */}
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
readOnly
|
||||
onFocus={(e) => e.target.removeAttribute('readonly')}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-sm text-destructive">{errors.email.message}</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* Password */}
|
||||
<Field>
|
||||
<div className="flex items-center">
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
className="pr-10"
|
||||
readOnly
|
||||
onFocus={(e) => e.target.removeAttribute('readonly')}
|
||||
{...register('password')}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPasswordVisible((prev) => !prev)}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{passwordVisible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="text-sm text-destructive">{errors.password.message}</p>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* Submit */}
|
||||
<Field>
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
Logging in...
|
||||
</span>
|
||||
) : 'Login'}
|
||||
</Button>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldDescription className="text-center font-medium">
|
||||
Don't have an account?{' '}
|
||||
<Link to="/signup" className="underline underline-offset-4">Sign up</Link>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
{/* Error Dialog */}
|
||||
<AlertDialog open={errorDialogOpen} onOpenChange={setErrorDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Login failed</AlertDialogTitle>
|
||||
<AlertDialogDescription>{errorMessage}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => setErrorDialogOpen(false)}>
|
||||
Okay
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Auth = () => {
|
||||
return (
|
||||
<div>
|
||||
Auth Page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { LoginForm } from "../components/LoginForm";
|
||||
import { MetadataProvider } from "@/contexts/MetadataContext";
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<MetadataProvider
|
||||
value={{
|
||||
title: "Login - Philproperties",
|
||||
description: "Kindly login to access our online course platform.",
|
||||
keywords: "login, philproperties, training, onboarding, real-estate",
|
||||
ogTitle: "Login - Philproperties",
|
||||
ogDescription: "Kindly login to access our online course platform.",
|
||||
}}
|
||||
>
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center">
|
||||
<Link to="/" className="flex items-center gap-2 font-medium">
|
||||
<div className="xs:w-40 sm:w-48 md:w-56 2xl:w-64 block dark:hidden">
|
||||
<img
|
||||
src="/philpro-white.png"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
<div className="xs:w-40 sm:w-48 md:w-56 2xl:w-64 hidden dark:block">
|
||||
<img
|
||||
src="/philpro-dark.png"
|
||||
alt="Image"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden lg:block">
|
||||
<img
|
||||
src="https://cq5as7pc73.ufs.sh/f/pHNnzIw3VjcgzIbC8SR4stTZRKXP3cfbD6e2p9jmdAUQBuox"
|
||||
alt="Image"
|
||||
className="absolute inset-0 h-full w-full object-cover rounded-3xl p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MetadataProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import PublicRoute from '../../../routes/PublicRoute'
|
||||
|
||||
import LandingLayout from '@/modules/public/layouts/LandingLayout'
|
||||
import LandingPage from '@/modules/public/pages/LandingPage'
|
||||
import Login from '../pages/Login'
|
||||
|
||||
|
||||
export const AuthRoutes = {
|
||||
element: <PublicRoute />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <Outlet />,
|
||||
children: [
|
||||
{ index: true, element: <LandingLayout><LandingPage /></LandingLayout> },
|
||||
{ path: "login", element: <Login />},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Client = () => {
|
||||
return (
|
||||
<div>
|
||||
Client Page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Client
|
||||
@@ -0,0 +1,9 @@
|
||||
import ProtectedRoute from '../../../routes/ProtectedRoute'
|
||||
import Client from '../pages/Client'
|
||||
|
||||
export const ClientRoutes = {
|
||||
element: <ProtectedRoute allowedRoles={['client']} />,
|
||||
children: [
|
||||
{ path: '/client', element: <Client /> },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: NavbarLayout.jsx
|
||||
* Type of Program: Layout Route
|
||||
* Description: A Layout router for Landing page.
|
||||
* Module: Public User
|
||||
* Author: lash0000
|
||||
* Date Created: Oct. 10, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG NUMBER DESCRIPTION
|
||||
* Oct. 10, 2025 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
***********************************************************************************************************************************************************************/
|
||||
|
||||
import { Fragment, useEffect, useRef } from 'react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { NavLink, Link } from 'react-router-dom';
|
||||
|
||||
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { RippleButton } from '@/components/custom/ripple-button';
|
||||
|
||||
import { Moon, Sun, Equal, Home, NotepadText, CircleUser, CroissantIcon, DoorOpen } from 'lucide-react';
|
||||
|
||||
import Footer from '../pages/LandingFooter';
|
||||
|
||||
function LandingLayout({ children }) {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const drawerCloseRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const isLargeScreen = window.matchMedia('(min-width: 1024px)').matches;
|
||||
if (isLargeScreen && drawerCloseRef.current) {
|
||||
drawerCloseRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const scrollToId = (id) => {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
const offset = 150
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - offset
|
||||
window.scrollTo({ top, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<nav className="fixed top-0 left-0 w-full h-fit flex items-center justify-between xs:px-6 lg:px-16 py-3 font-medium bg-background z-50 border">
|
||||
<div className="inline-flex items-center space-x-2 text-card-foreground selection:bg-card-foreground selection:text-white dark:selection:bg-card-foreground dark:selection:text-black">
|
||||
<div className="xs:w-32 lg:w-48">
|
||||
<img src="/philpro-white.png" alt="" className="block dark:hidden w-auto" />
|
||||
<img src="/philpro-dark.png" alt="" className="hidden dark:block w-auto" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className="xs:hidden lg:block">
|
||||
<div className="flex text-card-foreground selection:bg-card-foreground selection:text-white dark:selection:bg-card-foreground dark:selection:text-black gap-1.5">
|
||||
<Button asChild variant="ghost" className="px-3">
|
||||
<NavLink
|
||||
to="."
|
||||
onClick={() => scrollToId("home")}
|
||||
className={({ isActive }) => `${isActive ? 'underline' : ''}`}
|
||||
>
|
||||
Home
|
||||
</NavLink>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="px-3">
|
||||
<NavLink
|
||||
to="."
|
||||
onClick={() => scrollToId("about")}
|
||||
className={({ isActive }) => ` ${isActive ? 'underline' : ''}`}
|
||||
>
|
||||
About
|
||||
</NavLink>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="px-3">
|
||||
<NavLink
|
||||
to="."
|
||||
onClick={() => scrollToId("contacts")}
|
||||
className={({ isActive }) => `${isActive ? 'underline' : ''}`}
|
||||
>
|
||||
Contacts
|
||||
</NavLink>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" className="px-3">
|
||||
<NavLink
|
||||
to="."
|
||||
onClick={() => scrollToId("contacts")}
|
||||
className={({ isActive }) => ` ${isActive ? 'underline' : ''}`}
|
||||
>
|
||||
FAQs
|
||||
</NavLink>
|
||||
</Button>
|
||||
<Button className="dark:bg-blue-600 dark:text-white dark:hover:opacity-80" asChild>
|
||||
<NavLink
|
||||
to="/login"
|
||||
onClick={() => scrollToId("contacts")}
|
||||
className={({ isActive }) => `${isActive ? 'underline' : ''}`}
|
||||
>
|
||||
Sign in
|
||||
</NavLink>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={toggleTheme}
|
||||
className="text-card-foreground cursor-pointer"
|
||||
>
|
||||
{theme === 'light' ? (<> <Moon /> Dark </>) : (<> <Sun /> Light </>)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={toggleTheme}
|
||||
className="mr-3 text-card-foreground cursor-pointer lg:hidden"
|
||||
>
|
||||
{theme === 'light' ? (<> <Moon /> Dark </>) : (<> <Sun /> Light </>)}
|
||||
</Button>
|
||||
<DrawerTrigger asChild>
|
||||
<Button size="icon" className=" dark:bg-blue-500 dark:text-white lg:hidden">
|
||||
<Equal />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
||||
<DrawerContent className="font-geist">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle className="text-lg">
|
||||
Made with {String.fromCodePoint('0x1f499')}
|
||||
</DrawerTitle>
|
||||
<DrawerDescription>Explore more.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex flex-col gap-2 p-4 font-medium text-sm">
|
||||
<Link to="">
|
||||
<RippleButton rippleColor="#ADD8E6" className="w-full justify-start text-card-foreground" onClick={() => drawerCloseRef.current?.click()}>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<Home size={16} />
|
||||
<label>Home</label>
|
||||
</div>
|
||||
</RippleButton>
|
||||
</Link>
|
||||
<Link to="">
|
||||
<RippleButton rippleColor="#ADD8E6" className="w-full justify-start text-card-foreground" onClick={() => drawerCloseRef.current?.click()}>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<NotepadText size={16} />
|
||||
<label>About</label>
|
||||
</div>
|
||||
</RippleButton>
|
||||
</Link>
|
||||
<Link to="">
|
||||
<RippleButton rippleColor="#ADD8E6" className="w-full justify-start text-card-foreground" onClick={() => drawerCloseRef.current?.click()}>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<CircleUser size={16} />
|
||||
<label>Contacts</label>
|
||||
</div>
|
||||
</RippleButton>
|
||||
</Link>
|
||||
<Link to="">
|
||||
<RippleButton rippleColor="#ADD8E6" className="w-full justify-start text-card-foreground" onClick={() => drawerCloseRef.current?.click()}>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<CroissantIcon size={16} />
|
||||
<label>FAQs</label>
|
||||
</div>
|
||||
</RippleButton>
|
||||
</Link>
|
||||
<Link to="/login">
|
||||
<RippleButton className="dark:bg-blue-500 dark:text-white w-full justify-start bg-primary text-primary-foreground" onClick={() => drawerCloseRef.current?.click()}>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<DoorOpen size={16} />
|
||||
<label>Sign in</label>
|
||||
</div>
|
||||
</RippleButton>
|
||||
</Link>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose ref={drawerCloseRef} />
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
</nav>
|
||||
<main id="main-page" className='pt-28'>
|
||||
{children}
|
||||
<Toaster position="top-center" />
|
||||
</main>
|
||||
{/* add footer */}
|
||||
<Footer />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default LandingLayout;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: footer.jsx
|
||||
* Type of Program: Frontend Layout
|
||||
* Description: A footer page for landing page.
|
||||
* Module: Public
|
||||
* Author: lash0000
|
||||
* Date Created: Oct. 10, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG NUMBER DESCRIPTION
|
||||
* Oct. 10, 2025 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
***********************************************************************************************************************************************************************/
|
||||
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-background border-t">
|
||||
<div className="lg:container lg:mx-auto xs:px-6 lg:px-16 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-primary mb-4 text-2xl tracking-tighter">Resources</h3>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<Link to="">Philpro Learnings</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Company */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-primary mb-4 text-2xl tracking-tighter">Company</h3>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<Link to="">Philpro Learnings</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Socials */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-primary mb-4 text-2xl tracking-tighter">Socials</h3>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
<Link to="">Philpro Learnings</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="">Resources 1</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-start justify-start lg:justify-end">
|
||||
<div className="flex items-center gap-2 w-48">
|
||||
<img
|
||||
src="/philpro-white.png"
|
||||
alt="PhilProperties Logo"
|
||||
className="w-auto dark:hidden object-cover p-1"
|
||||
/>
|
||||
<img
|
||||
src="/philpro-dark.png"
|
||||
alt="PhilProperties Logo"
|
||||
className="hidden dark:block w-auto object-cover p-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* Copyright */}
|
||||
<div className="p-8 border-t">
|
||||
<p className="text-center text-muted-foreground text-sm">
|
||||
© {new Date().getFullYear()} Philproperties International Corporation. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: LandingPage.jsx
|
||||
* Type of Program: Frontend
|
||||
* Description: A frontend page for landing page.
|
||||
* Module: Public
|
||||
* Author: lash0000
|
||||
* Date Created: Oct. 10, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG NUMBER DESCRIPTION
|
||||
* Oct. 10, 2025 lash0000 001 Initial creation - STAR Phase 1 Project
|
||||
***********************************************************************************************************************************************************************/
|
||||
|
||||
import { Fragment, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/custom/original-tabs"
|
||||
import { InteractiveHoverButton } from "@/components/custom/interactive-hover-button"
|
||||
|
||||
import { Check, Megaphone, Sparkles, Send, BookOpen, CheckCheck, RefreshCw, Clipboard, GitCompare } from "lucide-react";
|
||||
|
||||
import { ContactData } from "@/data/landingPage.data"
|
||||
|
||||
function LandingPage() {
|
||||
const [copied, setCopied] = useState("")
|
||||
|
||||
const handleCopy = (text) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopied(text)
|
||||
setTimeout(() => setCopied(""), 3000)
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div id="home" className="lg:container lg:mx-auto lg:max-w-4xl flex xs:items-start md:items-center flex-col gap-8 pb-12">
|
||||
{/* Hero Section */}
|
||||
<div className="xs:px-8 xs:mt-0 lg:mt-12 w-full xs:items-start md:items-center justify-center flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="rounded-full">
|
||||
<BookOpen /> Online Courses
|
||||
</Badge>
|
||||
<Badge className="rounded-full">
|
||||
<Megaphone /> Sales
|
||||
</Badge>
|
||||
<Badge variant="outline" className="rounded-full">
|
||||
<GitCompare /> Alpha Testing
|
||||
</Badge>
|
||||
</div>
|
||||
<h1 className="xs:text-5xl lg:text-6xl font-bold tracking-tighter leading-tight text-primary max-w-2xl md:text-center break-words">
|
||||
Fueling Growth, Elevate your performance
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xl">Access the application, Achieve the transformation.</p>
|
||||
<div className="flex gap-4">
|
||||
<Button size="lg" className="cursor-pointer dark:bg-blue-600 dark:text-white dark:hover:opacity-80">
|
||||
<Sparkles /> Start Learning
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" className="cursor-pointer">
|
||||
<Send /> Explore
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banner Section */}
|
||||
<div className="border xs:w-full lg:w-[1040px]">
|
||||
<div className="xs:hidden lg:block relative bg-[#9B5DE0] e w-full h-[480px] overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('https://cq5as7pc73.ufs.sh/f/pHNnzIw3VjcggN4lGlIPcojb20WHEOIkxuly6JvSqsQZemMV')",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-8 left-1/2 -translate-x-1/2 bg-white rounded-full overflow-hidden">
|
||||
<Tabs defaultValue="tab-1" className="items-center">
|
||||
<TabsList className="gap-1 bg-muted">
|
||||
<TabsTrigger
|
||||
value="tab-1"
|
||||
className="group dark:data-[state=active]:text-blue-500 rounded-full data-[state=active]:shadow-none dark:data-[state=active]:border-blue-500 dark:data-[state=active]:border">
|
||||
<BookOpen
|
||||
className="-ms-0.5 me-1.5 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
To-do
|
||||
<Badge
|
||||
className="bg-primary dark:bg-blue-500 text-primary-foreground ms-2 min-w-5 rounded-full transition-opacity group-data-[state=inactive]:opacity-50 dark:group-data-[state=inactive]:bg-white"
|
||||
variant="secondary"
|
||||
>3</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tab-2"
|
||||
className="group dark:data-[state=active]:text-blue-500 rounded-full data-[state=active]:shadow-none dark:data-[state=active]:border-blue-500 dark:data-[state=active]:border">
|
||||
<RefreshCw
|
||||
className="-ms-0.5 me-1.5 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Pending
|
||||
<Badge
|
||||
className="bg-primary dark:bg-blue-500 text-primary-foreground ms-2 min-w-5 rounded-full transition-opacity group-data-[state=inactive]:opacity-50 dark:group-data-[state=inactive]:bg-white"
|
||||
variant="secondary"
|
||||
>8</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tab-3"
|
||||
className="group dark:data-[state=active]:text-blue-500 rounded-full data-[state=active]:shadow-none dark:data-[state=active]:border-blue-500 dark:data-[state=active]:border">
|
||||
<CheckCheck
|
||||
className="-ms-0.5 me-1.5 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Completed
|
||||
<Badge
|
||||
className="bg-primary dark:bg-blue-500 text-primary-foreground ms-2 min-w-5 -mr-1 rounded-full transition-opacity group-data-[state=inactive]:opacity-50 dark:group-data-[state=inactive]:bg-white"
|
||||
variant="secondary"
|
||||
>20</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 bg-muted rounded-tl-lg rounded-tr-lg p-2 pb-0 space-y-4 xs:w-[460px] lg:w-[540px]">
|
||||
<div className="border-t border-l border-r rounded-tl-lg rounded-tr-lg space-y-2 p-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge className="text-sm rounded-full px-4 py-1.5 bg-[#9b5de0]/20 dark:bg-green-500/20 text-[#9b5de0] dark:text-blue-500">
|
||||
Onboarding
|
||||
</Badge>
|
||||
<Badge className="text-sm rounded-full px-4 py-1.5 bg-[#9b5de0]/20 dark:bg-green-500/20 text-[#9b5de0] dark:text-blue-500">
|
||||
Checklist
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="font-bold tracking-tighter text-xl">Visit Philproperties Website</div>
|
||||
<Link to="https://philproperties.ph" target="_blank" className="text-[#9b5de0]">
|
||||
https://philproperties.ph/
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Call to Action */}
|
||||
<div id="about" className="xs:p-8 lg:p-12 flex flex-col xs:text-2xl lg:text-4xl font-bold tracking-tighter text-primary gap-8 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<div>
|
||||
“The Sales Training and Recruitment (STAR) makes building a winning sales team simple. From hiring the right people to fast-tracking their skills, it combines smart recruitment, clear onboarding, and practical training to create confident, high-performing professionals.”
|
||||
</div>
|
||||
<div>
|
||||
With Philproperties, you’re assured of faster onboarding, stronger sales talent, and a career journey built for growth. Less turnover, more success all in one system.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* For sales, why choose us? */}
|
||||
<div className="grid xs:grid-cols-1 lg:grid-cols-3 border-t">
|
||||
<div className="flex flex-col gap-4 xs:border-b lg:border-r xs:p-8 lg:p-12 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<h1 className="text-primary font-bold tracking-tighter text-4xl">Hire Smarter</h1>
|
||||
<p className="text-muted-foreground">Find the right talent faster with a streamlined recruitment process.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 xs:border-b lg:border-r xs:p-8 lg:p-12 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<h1 className="text-primary font-bold tracking-tighter text-4xl">Train Better</h1>
|
||||
<p className="text-muted-foreground">Equip every recruit with clear onboarding, mandatory modules, and practical sales training.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 xs:p-8 lg:p-12 xs:border-b hover:bg-muted dark:hover:bg-muted/20">
|
||||
<h1 className="text-primary font-bold tracking-tighter text-4xl">Grow</h1>
|
||||
<p className="text-muted-foreground">Build confident professionals, reduce turnover, and boost long-term sales performance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonials */}
|
||||
<div className="grid xs:grid-cols-1 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 lg:border-r xs:p-8 lg:p-12 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-primary font-bold tracking-tighter text-4xl">Frequently Asked Questions</h1>
|
||||
<p className="text-muted-foreground">Here are useful questions.</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<InteractiveHoverButton className="w-full rounded-md xs:text-left md:text-center lg:text-lg">
|
||||
<p>Is Philproperites real-estate firm?</p>
|
||||
</InteractiveHoverButton>
|
||||
<InteractiveHoverButton className="w-full rounded-md xs:text-xs xs:text-left md:text-center lg:text-lg">
|
||||
What this online course platform offers?
|
||||
</InteractiveHoverButton>
|
||||
<InteractiveHoverButton className="w-full rounded-md xs:text-xs xs:text-left md:text-center lg:text-lg">
|
||||
Premium plan offers?
|
||||
</InteractiveHoverButton>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contacts" className="flex flex-col gap-4 xs:p-8 lg:p-12 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-primary font-bold tracking-tighter text-4xl">Contact Us</h1>
|
||||
<p className="text-muted-foreground">Find the right talent faster with a streamlined recruitment process.</p>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{ContactData.map(({ id, icon: Icon, label, value }) => (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center justify-between bg-primary dark:bg-blue-600 rounded-md px-4 py-2 text-sm text-primary-foreground dark:text-white"
|
||||
>
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<Icon className="size-4" />
|
||||
{label && <span className="opacity-90">{label}</span>}
|
||||
<span className="font-medium ">{value}</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleCopy(value) + toast.success("Copied text successfully!")}
|
||||
>
|
||||
{copied === value ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<Clipboard className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Closing Remarks */}
|
||||
<div className="xs:p-8 lg:p-12 flex flex-col xs:text-4xl leading-tight border-t font-bold tracking-tighter text-primary gap-8 hover:bg-muted dark:hover:bg-muted/20">
|
||||
<div>
|
||||
Ready to supercharge? {<br />} Start by leveraging your limits.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingPage;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
export default function NotFound() {
|
||||
const { user } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const goHome = () => {
|
||||
switch (user?.acc_type) {
|
||||
case 'admin': navigate('/admin'); break
|
||||
case 'client': navigate('/client'); break
|
||||
case 'staff': navigate('/staff'); break
|
||||
default: navigate(-1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/40 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<p className="text-8xl font-bold text-muted-foreground/30">404</p>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Page not found</h1>
|
||||
<p className="text-muted-foreground">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={goHome}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-sm font-medium transition-colors"
|
||||
>
|
||||
Go to my dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
export default function Unauthorized() {
|
||||
const { user } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const goHome = () => {
|
||||
switch (user?.acc_type) {
|
||||
case 'admin': navigate('/admin'); break
|
||||
case 'client': navigate('/client'); break
|
||||
case 'staff': navigate('/staff'); break
|
||||
default: navigate(-1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/40 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<p className="text-8xl font-bold text-muted-foreground/30">403</p>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold">Access not authorized</h1>
|
||||
<p className="text-muted-foreground">
|
||||
You don't have permission to view this page.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={goHome}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-input bg-background hover:bg-accent hover:text-accent-foreground text-sm font-medium transition-colors"
|
||||
>
|
||||
Go to my dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Staff = () => {
|
||||
return (
|
||||
<div>
|
||||
Staff Page
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Staff
|
||||
@@ -0,0 +1,9 @@
|
||||
import ProtectedRoute from '../../../routes/ProtectedRoute'
|
||||
import Staff from '../pages/Staff'
|
||||
|
||||
export const StaffRoutes = {
|
||||
element: <ProtectedRoute allowedRoles={['staff']} />,
|
||||
children: [
|
||||
{ path: '/staff', element: <Staff /> },
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user