feat(source2): push all latest files - device labeling, help system, proxy docs, database isolation fix
- Added DOKUMENTASI-FILTER-PER-SITE.md (site isolation docs) - Fixed start-with-env.js to force-load .env.production - Fixed MONGODB_URI hostname from mongodb-netify to mongodb.prod.proit.id - Updated .gitignore to exclude sensitive scripts and credential files - Minor UI and labeling improvements
This commit is contained in:
1 parent
a403f752f3
commit
dd4c8f6876
385 files changed
+31016
-8268
No files matched your search
@@ -1,46 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
HelpCircle,
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
BookOpen,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTenantConfig } from "@/contexts/TenantConfigContext";
|
||||
import { FAQS } from "@/lib/helpContent";
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { HelpCircle, X, Search, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTenantConfig } from '@/contexts/TenantConfigContext';
|
||||
import { FAQS } from '@/lib/helpContent';
|
||||
|
||||
// ─── Expandable FAQ Item ───────────────────────────────────────────────────────
|
||||
const FAQItem = ({ question, answer }: { question: string; answer: string }) => {
|
||||
const FAQItem = ({ question, answer }: { question: string, answer: string }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
||||
return (
|
||||
<div className="border-b border-border/50 py-3 last:border-0">
|
||||
<button
|
||||
<div className="border-b border-border/50 py-3">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex w-full items-center justify-between text-left focus:outline-none gap-3"
|
||||
className="flex w-full items-center justify-between text-left focus:outline-none"
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground leading-snug">{question}</span>
|
||||
{isOpen ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-foreground">{question}</span>
|
||||
{isOpen ? <ChevronUp className="h-4 w-4 text-muted-foreground" /> : <ChevronDown className="h-4 w-4 text-muted-foreground" />}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<p className="pt-2 text-sm text-muted-foreground leading-relaxed">{answer}</p>
|
||||
<p className="pt-2 text-sm text-muted-foreground leading-relaxed">
|
||||
{answer}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -48,10 +36,19 @@ const FAQItem = ({ question, answer }: { question: string; answer: string }) =>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Main FAB Component ────────────────────────────────────────────────────────
|
||||
export function HelpCenterFAB() {
|
||||
const { tenantConfig } = useTenantConfig();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { tenantConfig } = useTenantConfig();
|
||||
|
||||
const brandName = tenantConfig?.brandName || "System";
|
||||
const footerCopyright = tenantConfig?.footerCopyright || `${brandName} © 2026`;
|
||||
|
||||
const filteredFaqs = FAQS.filter(
|
||||
faq =>
|
||||
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,10 +61,11 @@ export function HelpCenterFAB() {
|
||||
aria-label="Help and Resources"
|
||||
>
|
||||
<HelpCircle className="h-6 w-6" />
|
||||
<span className="absolute -inset-1 animate-ping rounded-full bg-blue-400 opacity-20" />
|
||||
{/* Pulse effect */}
|
||||
<span className="absolute -inset-1 animate-ping rounded-full bg-blue-400 opacity-20"></span>
|
||||
</motion.button>
|
||||
|
||||
{/* Drawer Overlay + Panel */}
|
||||
{/* Drawer Overlay */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
@@ -78,19 +76,20 @@ export function HelpCenterFAB() {
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="fixed inset-0 z-50 bg-background/40 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
|
||||
{/* Drawer Panel */}
|
||||
<motion.div
|
||||
initial={{ x: "100%" }}
|
||||
initial={{ x: '100%' }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||
exit={{ x: '100%' }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
||||
className="fixed right-0 top-0 z-50 flex h-full w-full max-w-sm flex-col bg-card/95 backdrop-blur-xl border-l border-border shadow-2xl"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-border/50 px-6 py-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-foreground">Help & Resources</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">User Guide & Documentation</p>
|
||||
<h2 className="text-lg font-bold text-foreground">Help & Resources</h2>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{brandName} Guide & Documentation</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
@@ -101,52 +100,46 @@ export function HelpCenterFAB() {
|
||||
</div>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-6">
|
||||
|
||||
{/* Full User Guide Banner */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4" /> Documentation
|
||||
</h3>
|
||||
<Link
|
||||
href="/help"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center justify-between gap-3 rounded-xl border border-blue-500/30 bg-blue-600/10 px-4 py-3 hover:bg-blue-600/20 transition-colors group"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-blue-500 dark:text-blue-400">
|
||||
Read Full User Guide
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Panduan membaca data per halaman, tab, dan kolom tabel
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-blue-500 shrink-0 group-hover:translate-x-0.5 transition-transform" />
|
||||
</Link>
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-8">
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search articles or FAQs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-xl border border-border bg-secondary/50 py-2.5 pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{/* FAQs */}
|
||||
<div className="space-y-2 pb-6">
|
||||
<div className="space-y-3 pb-8">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4" /> Frequently Asked Questions
|
||||
</h3>
|
||||
<div className="rounded-xl border border-border/50 bg-card px-4">
|
||||
{FAQS.map((faq, index) => (
|
||||
<FAQItem key={index} question={faq.question} answer={faq.answer} />
|
||||
))}
|
||||
{filteredFaqs.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-4 text-center">No matching FAQs found</p>
|
||||
) : (
|
||||
filteredFaqs.map((faq, index) => (
|
||||
<FAQItem key={index} question={faq.question} answer={faq.answer} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-border/50 bg-secondary/30 p-4 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{tenantConfig?.brandName || "DPI Dashboard"} © 2026
|
||||
<br />
|
||||
{tenantConfig?.footerCopyright || "PT. Data Bisnis Solusi"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground" dangerouslySetInnerHTML={{ __html: footerCopyright }} />
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user