feat: add admin and dashboard layouts with context for improved structure and functionality
This commit is contained in:
parent
2435f9a5d5
commit
8b1af335ec
@ -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
41
app/contexts/admin.tsx
Normal 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
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|||||||
10
app/layouts/admin/default.tsx
Normal file
10
app/layouts/admin/default.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
106
app/routes/_layout.admin.auth.login.tsx
Normal file
106
app/routes/_layout.admin.auth.login.tsx
Normal 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
|
||||||
@ -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
|
||||||
16
app/routes/_layout.admin.tsx
Normal file
16
app/routes/_layout.admin.tsx
Normal 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
|
||||||
@ -1,5 +0,0 @@
|
|||||||
const DashboardLayout = () => {
|
|
||||||
return <div>Dashboard</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DashboardLayout
|
|
||||||
Loading…
x
Reference in New Issue
Block a user