feat: add admin and dashboard layouts with context for improved structure and functionality

This commit is contained in:
Ardeman 2025-02-23 10:11:47 +08:00
parent 2435f9a5d5
commit 8b1af335ec
10 changed files with 188 additions and 17 deletions

View File

@ -1,9 +1,13 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@theme { @theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-admin: 'Poppins', ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
} }
html, html,

41
app/contexts/admin.tsx Normal file
View File

@ -0,0 +1,41 @@
import {
createContext,
useState,
useContext,
type PropsWithChildren,
type SetStateAction,
type Dispatch,
} from 'react'
type AdminProfile = {
name: string
}
type AdminContextProperties = {
adminProfile: AdminProfile
setAdminProfile: Dispatch<SetStateAction<AdminProfile>>
}
const AdminContext = createContext<AdminContextProperties | undefined>(
undefined,
)
export const AdminProvider = ({ children }: PropsWithChildren) => {
const [adminProfile, setAdminProfile] = useState<AdminProfile>({
name: '',
})
return (
<AdminContext.Provider value={{ adminProfile, setAdminProfile }}>
{children}
</AdminContext.Provider>
)
}
export const useAdminContext = (): AdminContextProperties => {
const context = useContext(AdminContext)
if (!context) {
throw new Error('useAdminContext must be used within a AdminProvider')
}
return context
}

View File

@ -1,15 +1,17 @@
import React, { import {
createContext, createContext,
useState, useState,
useContext, useContext,
type PropsWithChildren, type PropsWithChildren,
type Dispatch,
type SetStateAction,
} from 'react' } from 'react'
type NewsContextProperties = { type NewsContextProperties = {
isLoginOpen: boolean isLoginOpen: boolean
setIsLoginOpen: React.Dispatch<React.SetStateAction<boolean>> setIsLoginOpen: Dispatch<SetStateAction<boolean>>
isRegisterOpen: boolean isRegisterOpen: boolean
setIsRegisterOpen: React.Dispatch<React.SetStateAction<boolean>> setIsRegisterOpen: Dispatch<SetStateAction<boolean>>
} }
const NewsContext = createContext<NewsContextProperties | undefined>(undefined) const NewsContext = createContext<NewsContextProperties | undefined>(undefined)

View File

@ -0,0 +1,10 @@
import type { PropsWithChildren } from 'react'
export const AdminDefaultLayout = (properties: PropsWithChildren) => {
const { children } = properties
return (
<main className="font-admin relative min-h-dvh bg-[#F7F8FC]">
{children}
</main>
)
}

View File

@ -1,4 +1,4 @@
import type { JSX } from 'react' import type { JSX, SVGProps } from 'react'
import { FacebookIcon } from '~/components/icons/facebook' import { FacebookIcon } from '~/components/icons/facebook'
import { InstagramIcon } from '~/components/icons/instagram' import { InstagramIcon } from '~/components/icons/instagram'
@ -42,7 +42,7 @@ type FooterMenu = {
title: string title: string
url: string url: string
icon?: ( icon?: (
properties: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>, properties: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) => JSX.Element ) => JSX.Element
}> }>
} }

View File

@ -1,3 +1,4 @@
import type { ReactNode } from 'react'
import { import {
isRouteErrorResponse, isRouteErrorResponse,
Links, Links,
@ -18,10 +19,6 @@ export const links: Route.LinksFunction = () => [
href: 'https://fonts.gstatic.com', href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous', crossOrigin: 'anonymous',
}, },
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
},
] ]
export const meta = ({ location }: Route.MetaArgs) => { export const meta = ({ location }: Route.MetaArgs) => {
@ -39,7 +36,7 @@ export const meta = ({ location }: Route.MetaArgs) => {
] ]
} }
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>

View File

@ -0,0 +1,106 @@
import { useState } from 'react'
import { Link } from 'react-router'
import { EyeIcon } from '~/components/icons/eye'
import { Button } from '~/components/ui/button'
import { APP } from '~/data/meta'
const AuthLayout = () => {
const [showPassword, setShowPassword] = useState(false)
return (
<div className="flex min-h-dvh min-w-dvw flex-col items-center justify-center space-y-8">
<div className="grid max-w-lg items-center justify-center space-y-7 rounded-[20px] border border-[#E6E6E6] bg-white p-8">
<div className="flex flex-col items-center">
<img
src={APP.logo}
alt={APP.title}
className="h-[80px]"
/>
</div>
<p className="text-center">
Selamat Datang, silakan masukkan akun Anda untuk melanjutkan!
</p>
<div>
<form>
{/* Input Email / No Telepon */}
<div className="mb-4">
<label
htmlFor="email"
className="mb-1 block text-gray-700"
>
Email/No. Telepon
</label>
<input
type="text"
placeholder="Contoh: legal@legalgo.id"
className="focus:inheriten w-full rounded-md border border-[#DFDFDF] p-2"
/>
</div>
{/* Input Password */}
<div className="relative mb-4">
<label
htmlFor="password"
className="mb-1 block text-gray-700 focus:outline-[#2E2F7C]"
>
Kata Sandi
</label>
<input
type={showPassword ? 'text' : 'password'}
placeholder="Masukkan Kata Sandi"
className="w-full rounded-md border border-[#DFDFDF] p-2 pr-10 focus:outline-[#2E2F7C]"
/>
<button
type="button"
className="absolute top-9 right-3 text-gray-500"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeIcon
width={15}
height={15}
/>
) : (
<EyeIcon
width={15}
height={15}
/>
)}
</button>
</div>
{/* Lupa Kata Sandi */}
<div className="mb-4 flex justify-between">
<span className="text-gray-600">Lupa Kata Sandi?</span>
<Link
to="/admin/auth/reset-password"
className="font-semibold text-[#2E2F7C]"
>
Reset Kata Sandi
</Link>
</div>
{/* Tombol Masuk */}
<Button className="w-full rounded-md bg-[#2E2F7C] py-2 text-white transition hover:bg-blue-800">
Masuk
</Button>
</form>
</div>
</div>
{/* Link Daftar */}
<div className="mt-4 text-center text-sm">
Belum punya akun?{' '}
<Button
onClick={() => {}}
className="font-semibold text-[#2E2F7C]"
variant="link"
size="fit"
>
Daftar Disini
</Button>
</div>
</div>
)
}
export default AuthLayout

View File

@ -1,4 +1,4 @@
const AuthLayout = () => { const DashboardLayout = () => {
return ( return (
<div className="relative"> <div className="relative">
<div className="flex min-h-screen items-center justify-center bg-gray-100"> <div className="flex min-h-screen items-center justify-center bg-gray-100">
@ -7,4 +7,4 @@ const AuthLayout = () => {
</div> </div>
) )
} }
export default AuthLayout export default DashboardLayout

View File

@ -0,0 +1,16 @@
import { Outlet } from 'react-router'
import { AdminProvider } from '~/contexts/admin'
import { AdminDefaultLayout } from '~/layouts/admin/default'
const AdminLayout = () => {
return (
<AdminProvider>
<AdminDefaultLayout>
<Outlet />
</AdminDefaultLayout>
</AdminProvider>
)
}
export default AdminLayout

View File

@ -1,5 +0,0 @@
const DashboardLayout = () => {
return <div>Dashboard</div>
}
export default DashboardLayout