new changes

This commit is contained in:
Niranjan
2026-04-07 05:05:28 +05:30
parent 7c070224bd
commit a18bba15f2
29975 changed files with 3247495 additions and 2761 deletions

View File

@@ -1,56 +0,0 @@
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
import { Home, LogOut, Archive, Users } from 'lucide-react'
import { menuItems } from '../config/menu'
const iconMap: Record<string, React.ReactNode> = {
menuHome: <Home className="w-5 h-5" />,
menuBackupPlans: <Archive className="w-5 h-5" />,
menuUsers: <Users className="w-5 h-5" />,
menuLogout: <LogOut className="w-5 h-5" />,
}
export function Layout() {
const navigate = useNavigate()
return (
<div className="flex h-screen bg-gray-100 dark:bg-gray-900">
<aside className="w-56 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h1 className="text-xl font-bold text-gray-800 dark:text-white">YakPanel</h1>
</div>
<nav className="flex-1 overflow-y-auto p-2">
{menuItems
.filter((m) => m.id !== 'menuLogout')
.map((item) => (
<NavLink
key={item.id}
to={item.href}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 transition-colors ${
isActive
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
}`
}
>
{iconMap[item.id] || <span className="w-5 h-5" />}
<span>{item.title}</span>
</NavLink>
))}
</nav>
<div className="p-2 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => navigate('/logout')}
className="flex items-center gap-3 w-full px-3 py-2 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600"
>
<LogOut className="w-5 h-5" />
<span>Log out</span>
</button>
</div>
</aside>
<main className="flex-1 overflow-auto p-6">
<Outlet />
</main>
</div>
)
}

View File

@@ -0,0 +1,25 @@
export function AdminAlert({
variant = 'danger',
children,
className = '',
dismissible,
onDismiss,
}: {
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
children: React.ReactNode
className?: string
dismissible?: boolean
onDismiss?: () => void
}) {
return (
<div
className={`alert alert-${variant} ${dismissible ? 'alert-dismissible fade show' : ''} ${className}`.trim()}
role="alert"
>
{children}
{dismissible ? (
<button type="button" className="btn-close" aria-label="Close" onClick={onDismiss} />
) : null}
</div>
)
}

View File

@@ -0,0 +1,23 @@
export function AdminButton({
children,
variant = 'primary',
size,
type = 'button',
disabled,
className = '',
...rest
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' | 'outline-primary' | 'outline-secondary' | 'outline-danger'
size?: 'sm' | 'lg'
}) {
return (
<button
type={type}
disabled={disabled}
className={`btn btn-${variant}${size ? ` btn-${size}` : ''} ${className}`.trim()}
{...rest}
>
{children}
</button>
)
}

View File

@@ -0,0 +1,30 @@
export function AdminCard({
title,
iconClass,
children,
headerExtra,
className = '',
bodyClassName = '',
}: {
title?: React.ReactNode
iconClass?: string
children: React.ReactNode
headerExtra?: React.ReactNode
className?: string
bodyClassName?: string
}) {
return (
<div className={`card flex-fill ${className}`.trim()}>
{(title || headerExtra) && (
<div className="card-header border-0 pb-0 d-flex align-items-center justify-content-between flex-wrap gap-2">
<h4 className="mb-0 d-flex align-items-center gap-2">
{iconClass ? <i className={iconClass} aria-hidden /> : null}
{title}
</h4>
{headerExtra}
</div>
)}
<div className={`card-body ${bodyClassName}`.trim()}>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
export function AdminFormField({
label,
htmlFor,
hint,
error,
children,
className = '',
}: {
label: string
htmlFor?: string
hint?: string
error?: string
children: React.ReactNode
className?: string
}) {
return (
<div className={`mb-3 ${className}`.trim()}>
<label className="form-label" htmlFor={htmlFor}>
{label}
</label>
{children}
{hint && !error ? <div className="form-text">{hint}</div> : null}
{error ? <div className="invalid-feedback d-block">{error}</div> : null}
</div>
)
}
export function AdminSelect({
id,
value,
onChange,
children,
className = '',
disabled,
}: React.SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select
id={id}
className={`form-select ${className}`.trim()}
value={value}
onChange={onChange}
disabled={disabled}
>
{children}
</select>
)
}

View File

@@ -0,0 +1,214 @@
import { useEffect, useState } from 'react'
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
import Dropdown from 'react-bootstrap/Dropdown'
import { menuItems } from '../../config/menu'
import { useTheme } from '../../context/ThemeContext'
import { ThemeCustomizer } from './ThemeCustomizer'
const MINI_KEY = 'yakpanel_mini_sidebar'
export function AdminLayout() {
const navigate = useNavigate()
const { theme, toggleTheme } = useTheme()
const [mobileOpen, setMobileOpen] = useState(false)
const [miniSidebar, setMiniSidebar] = useState(() => localStorage.getItem(MINI_KEY) === '1')
useEffect(() => {
localStorage.setItem(MINI_KEY, miniSidebar ? '1' : '0')
}, [miniSidebar])
useEffect(() => {
if (miniSidebar) {
document.body.classList.add('mini-sidebar')
} else {
document.body.classList.remove('mini-sidebar')
}
}, [miniSidebar])
const closeMobile = () => setMobileOpen(false)
return (
<div className={`main-wrapper ${mobileOpen ? 'slide-nav' : ''}`}>
<div className="header">
<div className={`header-left ${miniSidebar ? '' : 'active'}`}>
<NavLink to="/" className="logo logo-normal" onClick={closeMobile}>
<img src="/theme/img/logo.png" alt="YakPanel" />
<img src="/theme/img/white-logo.png" className="white-logo" alt="" />
</NavLink>
<NavLink to="/" className="logo logo-small" onClick={closeMobile}>
<img src="/theme/img/logo-small.png" alt="" />
</NavLink>
<button
type="button"
id="toggle_btn"
className={miniSidebar ? '' : 'active'}
aria-label={miniSidebar ? 'Expand sidebar' : 'Collapse sidebar'}
onClick={() => setMiniSidebar(!miniSidebar)}
>
<i className="ti ti-arrow-bar-to-left" />
</button>
</div>
<button
type="button"
id="mobile_btn"
className="mobile_btn d-md-none btn btn-link p-0 border-0"
aria-label="Open menu"
onClick={() => setMobileOpen(!mobileOpen)}
>
<span className="bar-icon">
<span />
<span />
<span />
</span>
</button>
<div className="header-user">
<ul className="nav user-menu">
<li className="nav-item nav-search-inputs me-auto d-none d-md-block">
<div className="top-nav-search">
<form className="dropdown" onSubmit={(e) => e.preventDefault()}>
<div className="searchinputs">
<input type="search" className="form-control" placeholder="Search" aria-label="Search" />
<div className="search-addon">
<button type="submit" className="btn btn-link p-0" aria-label="Submit search">
<i className="ti ti-command" />
</button>
</div>
</div>
</form>
</div>
</li>
<li className="nav-item">
<button
type="button"
className="btn btn-link nav-link dark-mode-toggle p-0"
id="dark-mode-toggle"
aria-label="Toggle theme"
onClick={toggleTheme}
>
<i className={`ti ti-sun light-mode ${theme === 'light' ? 'active' : ''}`} />
<i className={`ti ti-moon dark-mode ${theme === 'dark' ? 'active' : ''}`} />
</button>
</li>
<li className="nav-item dropdown has-arrow main-drop">
<Dropdown align="end">
<Dropdown.Toggle as="a" className="btn btn-link nav-link user-link p-0 d-flex align-items-center text-decoration-none">
<span className="user-img">
<img src="/theme/img/profiles/avatar-14.jpg" alt="" className="rounded-circle" width={32} height={32} />
</span>
<span className="user-content d-none d-md-inline text-start ms-2">
<span className="user-name d-block fw-medium">Admin</span>
<span className="user-role text-muted small">Panel</span>
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-end">
<Dropdown.Item onClick={() => navigate('/users')}>
<i className="ti ti-users me-2" />
Users
</Dropdown.Item>
<Dropdown.Item onClick={() => navigate('/config')}>
<i className="ti ti-settings me-2" />
Settings
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item onClick={() => navigate('/logout')}>
<i className="ti ti-logout me-2" />
Log out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</li>
</ul>
</div>
<div className="dropdown mobile-user-menu d-md-none">
<Dropdown>
<Dropdown.Toggle as="a" className="nav-link dropdown-toggle">
<i className="ti ti-dots-vertical" />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item onClick={() => { navigate('/'); closeMobile() }}>
<i className="ti ti-layout-dashboard me-2" />
Dashboard
</Dropdown.Item>
<Dropdown.Item onClick={() => navigate('/logout')}>
<i className="ti ti-logout me-2" />
Log out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</div>
</div>
<div className="sidebar" id="sidebar">
<div className="sidebar-inner slimscroll">
<div id="sidebar-menu" className="sidebar-menu">
<ul>
<li className="clinicdropdown">
<NavLink to="/users" onClick={closeMobile}>
<img src="/theme/img/profiles/avatar-14.jpg" className="img-fluid rounded-circle" alt="" width={40} height={40} />
<div className="user-names">
<h5>Admin</h5>
<h6>YakPanel</h6>
</div>
</NavLink>
</li>
</ul>
<ul>
<li>
<h6 className="submenu-hdr">Main</h6>
<ul>
{menuItems
.filter((m) => m.id !== 'menuLogout')
.map((item) => (
<li key={item.id}>
<NavLink
to={item.href}
onClick={closeMobile}
className={({ isActive }) => (isActive ? 'active' : undefined)}
>
<i className={item.iconClass} />
<span>{item.title}</span>
</NavLink>
</li>
))}
</ul>
</li>
</ul>
<ul>
<li>
<button
type="button"
className="btn btn-link text-start w-100 text-decoration-none text-body py-2 border-0"
onClick={() => navigate('/logout')}
>
<i className="ti ti-logout me-2" />
Log out
</button>
</li>
</ul>
</div>
</div>
</div>
<div className="page-wrapper">
<div className="content">
<Outlet />
</div>
</div>
{mobileOpen ? (
<button
type="button"
className="position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-25 border-0 p-0 d-md-none"
style={{ zIndex: 1040 }}
aria-label="Close menu"
onClick={closeMobile}
/>
) : null}
<ThemeCustomizer />
</div>
)
}

View File

@@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react'
import { Modal } from 'react-bootstrap'
export function AdminModal({
show,
onHide,
title,
children,
footer,
size,
}: {
show: boolean
onHide: () => void
title: string
children: React.ReactNode
footer?: React.ReactNode
size?: 'sm' | 'lg' | 'xl'
}) {
return (
<Modal show={show} onHide={onHide} size={size} centered scrollable>
<Modal.Header closeButton>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>{children}</Modal.Body>
{footer ? <Modal.Footer>{footer}</Modal.Footer> : null}
</Modal>
)
}
/** Focus trap helper for simple confirm flows */
export function useConfirmFocus(show: boolean) {
const ref = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (show) ref.current?.focus()
}, [show])
return ref
}

View File

@@ -0,0 +1,13 @@
export function AdminTable({
children,
className = '',
responsive = true,
}: {
children: React.ReactNode
className?: string
responsive?: boolean
}) {
const table = <table className={`table table-hover ${className}`.trim()}>{children}</table>
if (responsive) return <div className="table-responsive">{table}</div>
return table
}

View File

@@ -0,0 +1,20 @@
export function EmptyState({
iconClass = 'ti ti-inbox',
title,
description,
action,
}: {
iconClass?: string
title: string
description?: string
action?: React.ReactNode
}) {
return (
<div className="text-center py-5 text-muted">
<i className={`${iconClass} display-4 d-block mb-3`} aria-hidden />
<h5 className="text-body">{title}</h5>
{description ? <p className="mb-3">{description}</p> : null}
{action}
</div>
)
}

View File

@@ -0,0 +1,44 @@
import { Link, useLocation } from 'react-router-dom'
import { titleForPath } from '../../config/routes-meta'
type Crumb = { label: string; path?: string }
export function PageHeader({
title,
breadcrumbs,
actions,
}: {
title?: string
breadcrumbs?: Crumb[]
actions?: React.ReactNode
}) {
const { pathname } = useLocation()
const pageTitle = title ?? titleForPath(pathname)
const crumbs: Crumb[] = breadcrumbs ?? [{ label: 'Home', path: '/' }, { label: pageTitle }]
return (
<div className="page-header mb-4">
<div className="row align-items-center">
<div className="col-md-6">
<h3 className="page-title">{pageTitle}</h3>
<nav aria-label="breadcrumb">
<ol className="breadcrumb mb-0">
{crumbs.map((c, i) => (
<li
key={`${c.label}-${i}`}
className={`breadcrumb-item${i === crumbs.length - 1 ? ' active' : ''}`}
{...(i === crumbs.length - 1 ? { 'aria-current': 'page' as const } : {})}
>
{c.path && i < crumbs.length - 1 ? <Link to={c.path}>{c.label}</Link> : c.label}
</li>
))}
</ol>
</nav>
</div>
{actions ? (
<div className="col-md-6 d-flex justify-content-md-end mt-2 mt-md-0">{actions}</div>
) : null}
</div>
</div>
)
}

View File

@@ -0,0 +1,46 @@
export function SkeletonLine({ className = '' }: { className?: string }) {
return <div className={`placeholder-glow ${className}`.trim()}>
<span className="placeholder col-12 rounded" style={{ height: '1rem' }} />
</div>
}
export function SkeletonCard() {
return (
<div className="card">
<div className="card-body">
<SkeletonLine className="mb-2" />
<SkeletonLine className="mb-2" />
<SkeletonLine className="w-75" />
</div>
</div>
)
}
export function SkeletonTable({ rows = 5, cols = 4 }: { rows?: number; cols?: number }) {
return (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
{Array.from({ length: cols }).map((_, i) => (
<th key={i}>
<span className="placeholder col-8 rounded" />
</th>
))}
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, r) => (
<tr key={r}>
{Array.from({ length: cols }).map((_, c) => (
<td key={c}>
<span className="placeholder col-12 rounded" />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -0,0 +1,237 @@
import { useCallback, useEffect, useId, useState } from 'react'
import { useTheme } from '../../context/ThemeContext'
const THEME_IMG = (n: string) => `/theme/img/theme/theme-${n}.svg`
export function ThemeCustomizer() {
const {
theme,
accent,
sidebarTheme,
sidebarBg,
setTheme,
setAccent,
setSidebarTheme,
setSidebarBg,
resetTheme,
} = useTheme()
const [open, setOpen] = useState(false)
const headingId = useId()
const close = useCallback(() => setOpen(false), [])
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') close()
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open, close])
return (
<>
<div className="sidebar-contact">
<button
type="button"
className="toggle-theme border-0 bg-transparent p-0"
aria-expanded={open}
aria-controls="yakpanel-theme-panel"
aria-label={open ? 'Close theme settings' : 'Open theme customizer'}
onClick={() => setOpen((v) => !v)}
>
<i className={`fa fa-cog ${open ? 'fa-spin' : ''}`} aria-hidden />
</button>
</div>
<div id="yakpanel-theme-panel" className={`sidebar-themesettings ${open ? 'open' : ''}`} role="dialog" aria-modal="true" aria-labelledby={headingId}>
<div className="themesettings-header">
<h4 id={headingId}>Theme Customizer</h4>
<button type="button" className="btn btn-link p-0 text-body" aria-label="Close" onClick={close}>
<i className="ti ti-x" aria-hidden />
</button>
</div>
<div className="themesettings-inner">
<div className="themesettings-content">
<h6>Layout</h6>
<div className="row g-2">
<div className="col-6">
<ThemeImageRadio
name="yak-theme-layout"
value="light"
checked={theme === 'light'}
onChange={() => setTheme('light')}
id="yak-lightTheme"
label="Light"
imgSrc={THEME_IMG('01')}
/>
</div>
<div className="col-6">
<ThemeImageRadio
name="yak-theme-layout"
value="dark"
checked={theme === 'dark'}
onChange={() => setTheme('dark')}
id="yak-darkTheme"
label="Dark"
imgSrc={THEME_IMG('02')}
/>
</div>
</div>
</div>
<div className="themesettings-content">
<h6>Colors</h6>
<div className="theme-colorsset">
<ul className="mb-0">
{(
[
{ v: 'red' as const, id: 'yak-redColor' },
{ v: 'yellow' as const, id: 'yak-yellowColor' },
{ v: 'blue' as const, id: 'yak-blueColor' },
{ v: 'green' as const, id: 'yak-greenColor' },
] as const
).map(({ v, id }) => (
<li key={v}>
<div className="input-themeselects">
<input type="radio" name="yak-accent" id={id} value={v} checked={accent === v} onChange={() => setAccent(v)} />
<label htmlFor={id} className={`${v}-clr`} title={v}>
<span className="visually-hidden">{v}</span>
</label>
</div>
</li>
))}
</ul>
</div>
</div>
<div className="themesettings-content">
<h6>Sidebar</h6>
<div className="row g-2">
{(
[
{ v: 'light' as const, n: '03', label: 'Light' },
{ v: 'dark' as const, n: '04', label: 'Dark' },
{ v: 'blue' as const, n: '05', label: 'Blue' },
{ v: 'green' as const, n: '06', label: 'Green' },
] as const
).map(({ v, n, label }) => (
<div className="col-6" key={v}>
<ThemeImageRadio
name="yak-sidebar-style"
value={v}
checked={sidebarTheme === v}
onChange={() => setSidebarTheme(v)}
id={`yak-sidebar-${v}`}
label={label}
imgSrc={THEME_IMG(n)}
/>
</div>
))}
</div>
</div>
<div className="themesettings-content m-0 border-0">
<h6>Sidebar background</h6>
<div className="row g-2">
<div className="col-6">
<div className="input-themeselect">
<input
type="radio"
name="yak-sidebarbg"
id="yak-sidebarBgNone"
value="sidebarbgnone"
checked={sidebarBg === 'sidebarbgnone'}
onChange={() => setSidebarBg('sidebarbgnone')}
/>
<label htmlFor="yak-sidebarBgNone" className="d-flex align-items-center justify-content-center bg-body-secondary rounded" style={{ minHeight: 72 }}>
<span className="small text-muted">Default</span>
</label>
</div>
</div>
{(
[
{ v: 'sidebarbg1' as const, n: '07' },
{ v: 'sidebarbg2' as const, n: '08' },
{ v: 'sidebarbg3' as const, n: '09' },
{ v: 'sidebarbg4' as const, n: '10' },
] as const
).map(({ v, n }) => (
<div className="col-6" key={v}>
<div className="input-themeselect">
<input
type="radio"
name="yak-sidebarbg"
id={`yak-${v}`}
value={v}
checked={sidebarBg === v}
onChange={() => setSidebarBg(v)}
/>
<label htmlFor={`yak-${v}`}>
<img src={THEME_IMG(n)} alt="" />
<span className="w-100">
<span>Bg {n.slice(-1)}</span>
<span className="checkboxs-theme" />
</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
<div className="themesettings-footer">
<ul className="mb-0">
<li>
<button type="button" className="btn btn-cancel close-theme btn-light border w-100" onClick={close}>
Cancel
</button>
</li>
<li>
<button
type="button"
className="btn btn-reset btn-primary w-100"
onClick={() => {
resetTheme()
}}
>
Reset
</button>
</li>
</ul>
</div>
</div>
</>
)
}
function ThemeImageRadio<T extends string>({
name,
value,
checked,
onChange,
id,
label,
imgSrc,
}: {
name: string
value: T
checked: boolean
onChange: () => void
id: string
label: string
imgSrc: string
}) {
return (
<div className="input-themeselect">
<input type="radio" name={name} id={id} value={value} checked={checked} onChange={onChange} />
<label htmlFor={id}>
<img src={imgSrc} alt="" />
<span className="w-100">
<span>{label}</span>
<span className="checkboxs-theme" />
</span>
</label>
</div>
)
}

View File

@@ -0,0 +1,10 @@
export { AdminAlert } from './AdminAlert'
export { AdminButton } from './AdminButton'
export { AdminCard } from './AdminCard'
export { AdminFormField, AdminSelect } from './AdminFormField'
export { AdminModal, useConfirmFocus } from './AdminModal'
export { AdminTable } from './AdminTable'
export { EmptyState } from './EmptyState'
export { PageHeader } from './PageHeader'
export { SkeletonCard, SkeletonLine, SkeletonTable } from './Skeleton'
export { AdminLayout } from './AdminLayout'