This commit is contained in:
rgrgogu
2026-05-05 23:16:22 +08:00
parent 683b25956a
commit 745c51682e
125 changed files with 18925 additions and 2 deletions
@@ -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>
);
}
+93
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
const Admin = () => {
return (
<div>
Admin Page
</div>
)
}
export default Admin
+31
View File
@@ -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
]
},
],
}
+195
View File
@@ -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>
</>
)
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
const Auth = () => {
return (
<div>
Auth Page
</div>
)
}
export default Auth
+50
View File
@@ -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>
)
}
+21
View File
@@ -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 />},
]
},
],
}
+11
View File
@@ -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;
+104
View File
@@ -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">
&copy; {new Date().getFullYear()} Philproperties International Corporation. All rights reserved.
</p>
</div>
</footer>
)
}
+234
View File
@@ -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;
+36
View File
@@ -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>
)
}
+36
View File
@@ -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>
)
}
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
const Staff = () => {
return (
<div>
Staff Page
</div>
)
}
export default Staff
+9
View File
@@ -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 /> },
],
}