feat: improve header responsiveness and update seed initialization

- Add text truncation for title on mobile screens
- Hide user info section on mobile, show simplified icons only
- Update seed.ts to create admin and demo users with proper password hashing
- Add bcryptjs for password hashing in seed script
- Update QWEN.md documentation with seed command and default users

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-19 10:14:21 +08:00
parent 6c3e7c86b6
commit 5801eb4596
39 changed files with 3335 additions and 1834 deletions

View File

@@ -1,248 +1,435 @@
import { Container, Grid, Title, Text, SimpleGrid, Box, Accordion, Stack, useMantineColorScheme } 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';
import {
Accordion,
Box,
Container,
Grid,
SimpleGrid,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBook,
IconFileText,
IconHeadphones,
IconHelpCircle,
IconMessage,
IconVideo,
} from "@tabler/icons-react";
import { useState } from "react";
import { HelpCard } from "@/components/ui/help-card";
const HelpPage = () => {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// 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 { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
// 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 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 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 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' },
];
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);
// 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;
const handleSendMessage = () => {
if (inputValue.trim() === "") return;
// Add user message
const newUserMessage = {
id: messages.length + 1,
text: inputValue,
sender: 'user'
};
// Add user message
const newUserMessage = {
id: messages.length + 1,
text: inputValue,
sender: "user",
};
setMessages(prev => [...prev, newUserMessage]);
setInputValue('');
setIsLoading(true);
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);
};
// 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();
}
};
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>
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} bg={dark ? "#141D34" : "white"} p="lg" style={{ textAlign: 'center', borderColor: dark ? "#141D34" : "white" }} h="100%" >
<Text size="xl" fw={700} style={{ fontSize: '32px' }}>{stat.value}</Text>
<Text size="sm" color="dimmed">{stat.label}</Text>
</HelpCard>
))}
</SimpleGrid>
{/* Statistics Section */}
<SimpleGrid cols={3} spacing="lg" mb="xl">
{stats.map((stat, index) => (
<HelpCard
key={index}
bg={dark ? "#141D34" : "white"}
p="lg"
style={{
textAlign: "center",
borderColor: dark ? "#141D34" : "white",
}}
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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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>
<Stack gap="lg">
<Box>
<Grid gutter="lg" justify="center">
{/* Panduan Memulai */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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>
{/* Video Tutorial */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconHelpCircle size={24} />} title="FAQ" h="100%">
<Accordion variant="separated" >
{faqItems.map((item, index) => (
<Accordion.Item style={{ backgroundColor: dark ? "#263852ff" : "#F1F5F9" }} 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>
{/* FAQ */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
icon={<IconHelpCircle size={24} />}
title="FAQ"
h="100%"
>
<Accordion variant="separated">
{faqItems.map((item, index) => (
<Accordion.Item
style={{
backgroundColor: dark ? "#263852ff" : "#F1F5F9",
}}
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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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>
<Box>
<Grid>
{/* Hubungi Support */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
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}>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}>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>
<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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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>
{/* Dokumentasi */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
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 style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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' ? dark ? "#263852ff" : "#F1F5F9" : dark ? "#263852ff" : "#F1F5F9",
color: msg.sender === 'user' ? dark ? "#F1F5F9" : "#263852ff" : dark ? "#F1F5F9" : "#263852ff",
padding: '8px 12px',
borderRadius: '8px',
marginBottom: '8px',
maxWidth: '80%'
}}
>
{msg.text}
</Box>
))}
</Box>
{/* Jenna - Virtual Assistant */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard
style={{ borderColor: dark ? "#141D34" : "white" }}
bg={dark ? "#141D34" : "white"}
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"
? dark
? "#263852ff"
: "#F1F5F9"
: dark
? "#263852ff"
: "#F1F5F9",
color:
msg.sender === "user"
? dark
? "#F1F5F9"
: "#263852ff"
: dark
? "#F1F5F9"
: "#263852ff",
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>
);
<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;
export default HelpPage;