feat: implement help page with equal height cards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-13 11:34:43 +08:00
parent 4ed1c664d1
commit ab2afbb27f
5 changed files with 817 additions and 0 deletions

453
Dashboard-MD/BANTUAN.md Normal file
View File

@@ -0,0 +1,453 @@
Design Contract Halaman Bantuan / Support Center
Dokumen ini adalah kontrak desain UI/UX untuk halaman Bantuan / Support Center.
Implementasi tidak dapat dilanjutkan tanpa mengikuti spesifikasi ini secara konsisten.
1. Tujuan Halaman
Halaman Bantuan bertujuan untuk:
Memberikan akses cepat ke panduan & dokumentasi
Menyediakan video tutorial
Menjawab pertanyaan umum (FAQ)
Menyediakan kontak support
Menyediakan virtual assistant (chat)
Menampilkan statistik kredibilitas platform
2. Struktur Layout Keseluruhan
Layout Grid
Menggunakan 3-column responsive grid.
Desktop (≥ 1200px)
Grid: 3 columns
Gap: 32px
Container max-width: 1200px 1320px
Center aligned
Tablet (768px 1199px)
Grid: 2 columns
Gap: 24px
Mobile (< 768px)
Grid: 1 column
Gap: 16px
Card full width
3. Struktur Komponen UI
3.1 Card Utama (Reusable Component)
Semua section menggunakan komponen dasar:
Card
├── Icon Container
├── Title
├── List / Content
Spesifikasi Card
Border radius: 16px
Padding: 24px
Shadow:
Light: soft shadow
Dark: subtle glow / low-opacity shadow
Transition hover: 0.2s ease
Hover effect:
Slight lift (translateY(-2px))
Elevation shadow increase
4. Daftar Komponen
4.1 Panduan Memulai
Isi:
Cara Login
Navigasi Dashboard
Fitur Dasar
Tips & Trik
Interaksi:
Klik item navigasi ke halaman detail
Cursor pointer
Hover underline atau highlight
4.2 Video Tutorial
Isi:
Dashboard Overview
Analisis Data
Membuat Laporan
Export Data
Interaksi:
Klik buka modal video atau halaman video
Bisa tambahkan icon play kecil di tiap item
4.3 FAQ
Isi:
Masalah Login
Reset Password
Akses Data
Laporan Error
Interaksi:
Accordion expand/collapse
Animasi height smooth (200300ms)
4.4 Hubungi Support
Isi:
Email
WhatsApp
Jam Kerja
Waktu Respon
Interaksi:
Email mailto link
WhatsApp wa.me link
Jam kerja non-clickable
4.5 Dokumentasi
Isi:
API Reference
Integrasi Sistem
Format Data
Best Practices
Interaksi:
Klik navigasi dokumentasi
4.6 Jenna Virtual Assistant
Komponen:
Chat Container
├── Header Title
├── Chat Message Area
├── Input Field
└── Send Button
Spesifikasi:
Tinggi tetap 300350px)
Scrollable message container
Input rounded
Send button icon (arrow)
Interaksi:
Enter kirim pesan
Klik send kirim pesan
Auto-scroll ke pesan terakhir
Disabled state saat loading
4.7 Statistik Section
3 Card horizontal:
150+ Artikel Panduan
50+ Video Tutorial
24/7 Support Aktif
Spesifikasi:
Center aligned text
Angka besar (font-size 3240px)
Subtext kecil opacity 70%
5. Design System
5.1 Light Mode
Background
Page background: #F5F7FA
Card background: #FFFFFF
Primary Color
Blue: #3B82F6
Hover: #2563EB
Text
Primary: #111827
Secondary: #6B7280
Border
#E5E7EB
Shadow
0 4px 12px rgba(0,0,0,0.05)
Icon Container
Background: #EFF6FF
Icon color: #2563EB
5.2 Dark Mode
Background
Page background: #0F172A
Card background: #1E293B
Primary Color
Blue: #3B82F6
Hover: #60A5FA
Text
Primary: #F8FAFC
Secondary: #94A3B8
Border
#334155
Shadow
0 4px 20px rgba(0,0,0,0.4)
Icon Container
Background: #1D4ED8
Icon color: #FFFFFF
6. Typography
Font Family:
Inter / Poppins / System UI
Hierarchy:
Element Size Weight
Section Title 1820px 600
Card Title 1618px 600
List Item 1416px 400
Statistik Number 3240px 700
Line height:
1.5 standard
1.2 for large numbers
7. Spacing System
Gunakan sistem 8pt grid:
8px
16px
24px
32px
48px
Padding Card: 24px
Gap Grid Desktop: 32px
8. Responsivitas
Desktop
3 kolom utama
Statistik 3 sejajar
Tablet
2 kolom
Statistik 2 + 1
Mobile
1 kolom
Statistik stacked vertical
Chat full width
9. State & Interactivity Requirements
Harus tersedia:
Hover state
Active state
Focus state (accessibility)
Disabled state
Loading state (chat)
Keyboard support:
Tab navigable
Enter kirim pesan
Esc tutup modal (jika ada)
10. Accessibility
Minimum contrast ratio 4.5:1
Focus ring visible
Button min-height 40px
Click area minimal 44x44px
11. Animasi
Durasi standar: 200ms 300ms
Easing: ease-in-out
Digunakan untuk:
Hover card
Accordion FAQ
Chat message appear
Button press
12. Toggle Dark / Light Mode
Harus tersedia:
Theme switcher
Persist ke localStorage
Default mengikuti system preference
13. Data Dinamis
Data yang harus bisa dinamis:
Jumlah artikel
Jumlah video
Status support 24/7
Chat message list
FAQ content
Dokumentasi link
14. Non-Functional Requirements
Clean modular component
Reusable Card component
Maintainable theme config
Dark & Light share same structure
15. Kesimpulan
Halaman ini menggunakan:
Card-based layout
Grid responsive
Dual theme (Light & Dark)
High clarity & readability
Modern SaaS style
Virtual assistant as engagement point
Implementasi wajib mengikuti spesifikasi ini agar:
Konsistensi visual terjaga
User experience optimal
Maintainability tinggi
Skalabilitas mudah

View File

@@ -0,0 +1,246 @@
import { Container, Grid, Title, Text, SimpleGrid, Box, Accordion, Stack } from '@mantine/core';
import { HelpCard } from '@/components/ui/help-card';
import { IconBook, IconVideo, IconHelpCircle, IconMessage, IconFileText, IconHeadphones } from '@tabler/icons-react';
import { useState } from 'react';
const HelpPage = () => {
// Sample data for sections
const guideItems = [
{ title: 'Cara Login', description: 'Langkah-langkah untuk login ke dashboard' },
{ title: 'Navigasi Dashboard', description: 'Penjelasan tentang tata letak dan navigasi' },
{ title: 'Fitur Dasar', description: 'Panduan penggunaan fitur-fitur utama' },
{ title: 'Tips & Trik', description: 'Tips untuk meningkatkan produktivitas' },
];
const videoItems = [
{ title: 'Dashboard Overview', duration: '5:23' },
{ title: 'Analisis Data', duration: '8:45' },
{ title: 'Membuat Laporan', duration: '6:12' },
{ title: 'Export Data', duration: '4:30' },
];
const faqItems = [
{ question: 'Bagaimana cara reset password?', answer: 'Anda dapat mereset password melalui halaman login dengan klik "Lupa Password"' },
{ question: 'Apakah saya bisa mengakses data offline?', answer: 'Saat ini aplikasi hanya dapat diakses secara online' },
{ question: 'Berapa lama waktu respon support?', answer: 'Tim support kami biasanya merespon dalam waktu kurang dari 24 jam' },
{ question: 'Bagaimana cara menambahkan pengguna baru?', answer: 'Fitur penambahan pengguna dapat ditemukan di menu Pengaturan > Manajemen Pengguna' },
];
const documentationItems = [
{ title: 'API Reference', description: 'Dokumentasi lengkap untuk integrasi API' },
{ title: 'Integrasi Sistem', description: 'Cara mengintegrasikan dengan sistem eksternal' },
{ title: 'Format Data', description: 'Spesifikasi format data yang didukung' },
{ title: 'Best Practices', description: 'Praktik terbaik dalam penggunaan platform' },
];
const stats = [
{ value: '150+', label: 'Artikel Panduan' },
{ value: '50+', label: 'Video Tutorial' },
{ value: '24/7', label: 'Support Aktif' },
];
// State for chat functionality
const [messages, setMessages] = useState([
{ id: 1, text: 'Halo! Saya Jenna, asisten virtual Anda. Bagaimana saya bisa membantu hari ini?', sender: 'jenna' }
]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSendMessage = () => {
if (inputValue.trim() === '') return;
// Add user message
const newUserMessage = {
id: messages.length + 1,
text: inputValue,
sender: 'user'
};
setMessages(prev => [...prev, newUserMessage]);
setInputValue('');
setIsLoading(true);
// Simulate Jenna's response after delay
setTimeout(() => {
const jennaResponse = {
id: messages.length + 2,
text: 'Terima kasih atas pertanyaan Anda. Saat ini saya adalah versi awal dari asisten virtual. Tim kami sedang mengembangkan kemampuan saya lebih lanjut.',
sender: 'jenna'
};
setMessages(prev => [...prev, jennaResponse]);
setIsLoading(false);
}, 1000);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
return (
<Container size="lg" py="xl">
<Title order={1} mb="xl" ta="center">Pusat Bantuan</Title>
<Text size="lg" color="dimmed" ta="center" mb="xl">
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
</Text>
{/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => (
<HelpCard key={index} p="lg" style={{ textAlign: 'center' }} h="100%">
<Text size="xl" fw={700} style={{ fontSize: '32px' }}>{stat.value}</Text>
<Text size="sm" color="dimmed">{stat.label}</Text>
</HelpCard>
))}
</SimpleGrid>
<Stack gap="lg">
<Box>
<Grid gutter="lg" justify="center">
{/* Panduan Memulai */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconBook size={24} />} title="Panduan Memulai" h="100%">
<Box>
{guideItems.map((item, index) => (
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Navigasi ke ${item.title}`)}>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">{item.description}</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
{/* Video Tutorial */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconVideo size={24} />} title="Video Tutorial" h="100%">
<Box>
{videoItems.map((item, index) => (
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Buka video: ${item.title}`)}>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">{item.duration}</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
{/* FAQ */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconHelpCircle size={24} />} title="FAQ" h="100%">
<Accordion variant="separated">
{faqItems.map((item, index) => (
<Accordion.Item key={index} value={`faq-${index}`}>
<Accordion.Control>{item.question}</Accordion.Control>
<Accordion.Panel>
<Text size="sm">{item.answer}</Text>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</HelpCard>
</Grid.Col>
</Grid>
</Box>
<Box>
<Grid>
{/* Hubungi Support */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconHeadphones size={24} />} title="Hubungi Support" h="100%">
<Box>
<Text fw={500}>Email</Text>
<Text size="sm" color="dimmed" mb="md"><a href="mailto:support@example.com">support@example.com</a></Text>
<Text fw={500}>WhatsApp</Text>
<Text size="sm" color="dimmed" mb="md"><a href="https://wa.me/1234567890">+62 123 456 7890</a></Text>
<Text fw={500}>Jam Kerja</Text>
<Text size="sm" color="dimmed">Senin - Jumat, 09:00 - 17:00 WIB</Text>
<Text fw={500} mt="md">Waktu Respon</Text>
<Text size="sm" color="dimmed">Rata-rata 2-4 jam kerja</Text>
</Box>
</HelpCard>
</Grid.Col>
{/* Dokumentasi */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconFileText size={24} />} title="Dokumentasi" h="100%">
<Box>
{documentationItems.map((item, index) => (
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Navigasi ke dokumentasi: ${item.title}`)}>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">{item.description}</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
{/* Jenna - Virtual Assistant */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard icon={<IconMessage size={24} />} title="Jenna - Virtual Assistant" h="100%">
<Box style={{ height: '300px', display: 'flex', flexDirection: 'column' }}>
<Box style={{ flex: 1, overflowY: 'auto', marginBottom: '12px', maxHeight: '200px' }}>
{messages.map((msg) => (
<Box
key={msg.id}
style={{
alignSelf: msg.sender === 'user' ? 'flex-end' : 'flex-start',
backgroundColor: msg.sender === 'user' ? '#3B82F6' : '#F3F4F6',
color: msg.sender === 'user' ? 'white' : 'black',
padding: '8px 12px',
borderRadius: '8px',
marginBottom: '8px',
maxWidth: '80%'
}}
>
{msg.text}
</Box>
))}
</Box>
<Box style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ketik pesan Anda..."
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '20px',
border: '1px solid #ccc',
}}
disabled={isLoading}
/>
<button
onClick={handleSendMessage}
disabled={isLoading || inputValue.trim() === ''}
style={{
padding: '8px 16px',
borderRadius: '20px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
cursor: 'pointer',
}}
>
Kirim
</button>
</Box>
</Box>
</HelpCard>
</Grid.Col>
</Grid>
</Box>
</Stack>
</Container>
);
};
export default HelpPage;

View File

@@ -0,0 +1,90 @@
import { Card, useMantineTheme, useComputedColorScheme } from '@mantine/core';
import type { CardProps } from '@mantine/core';
import type { ReactNode } from 'react';
interface HelpCardProps extends CardProps {
children: ReactNode;
icon?: ReactNode;
title?: string;
minHeight?: string | number; // Allow specifying a minimum height
}
export const HelpCard = ({
children,
icon,
title,
minHeight = 'auto', // Default to auto, but allow override
...props
}: HelpCardProps) => {
const theme = useMantineTheme();
const colorScheme = useComputedColorScheme('light');
const isDark = colorScheme === 'dark';
return (
<Card
shadow="sm"
padding="xl"
radius="md"
withBorder
style={{
backgroundColor: isDark ? theme.colors.dark[7] : theme.white,
borderRadius: '16px',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
border: `1px solid ${
isDark ? theme.colors.dark[4] : theme.colors.gray[3]
}`,
minHeight, // Apply the minimum height
display: 'flex',
flexDirection: 'column',
}}
{...props}
>
{(icon || title) && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
marginBottom: '16px',
}}
>
{icon && (
<div
style={{
backgroundColor: isDark
? theme.colors.blue[8]
: theme.colors.blue[0],
borderRadius: '8px',
padding: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{icon}
</div>
)}
{title && (
<h3
style={{
margin: 0,
fontSize: '16px',
fontWeight: 600,
color: isDark
? theme.colors.dark[0]
: theme.colors.dark[9],
}}
>
{title}
</h3>
)}
</div>
)}
<div style={{ flex: 1 }}>
{children}
</div>
</Card>
);
};

View File

@@ -28,6 +28,7 @@ import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keaman
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
import { Route as DashboardBantuanRouteImport } from './routes/dashboard/bantuan'
import { Route as AdminUsersRouteImport } from './routes/admin/users'
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
@@ -130,6 +131,11 @@ const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
path: '/bumdes',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardBantuanRoute = DashboardBantuanRouteImport.update({
id: '/bantuan',
path: '/bantuan',
getParentRoute: () => DashboardRouteRoute,
} as any)
const AdminUsersRoute = AdminUsersRouteImport.update({
id: '/users',
path: '/users',
@@ -155,6 +161,7 @@ export interface FileRoutesByFullPath {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
@@ -177,6 +184,7 @@ export interface FileRoutesByTo {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
@@ -202,6 +210,7 @@ export interface FileRoutesById {
'/admin/apikey': typeof AdminApikeyRoute
'/admin/settings': typeof AdminSettingsRoute
'/admin/users': typeof AdminUsersRoute
'/dashboard/bantuan': typeof DashboardBantuanRoute
'/dashboard/bumdes': typeof DashboardBumdesRoute
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
@@ -228,6 +237,7 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
@@ -250,6 +260,7 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
@@ -274,6 +285,7 @@ export interface FileRouteTypes {
| '/admin/apikey'
| '/admin/settings'
| '/admin/users'
| '/dashboard/bantuan'
| '/dashboard/bumdes'
| '/dashboard/demografi-pekerjaan'
| '/dashboard/jenna-analytic'
@@ -437,6 +449,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardBumdesRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/bantuan': {
id: '/dashboard/bantuan'
path: '/bantuan'
fullPath: '/dashboard/bantuan'
preLoaderRoute: typeof DashboardBantuanRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/admin/users': {
id: '/admin/users'
path: '/users'
@@ -480,6 +499,7 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
)
interface DashboardRouteRouteChildren {
DashboardBantuanRoute: typeof DashboardBantuanRoute
DashboardBumdesRoute: typeof DashboardBumdesRoute
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
@@ -492,6 +512,7 @@ interface DashboardRouteRouteChildren {
}
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
DashboardBantuanRoute: DashboardBantuanRoute,
DashboardBumdesRoute: DashboardBumdesRoute,
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,

View File

@@ -0,0 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import HelpPage from '@/components/help-page'
export const Route = createFileRoute('/dashboard/bantuan')({
component: HelpPage,
})