add navigation on internify header: navigation to home from humic logo +
crosscheck line code
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// src/components/common/Avatar.tsx
|
||||
|
||||
export default function Avatar({ initials }: { initials: string }) {
|
||||
return (
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center text-xs text-white flex-shrink-0 bg-primary">
|
||||
|
||||
+106
-108
@@ -1,108 +1,106 @@
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import useBreadcrumbs from "use-react-router-breadcrumbs";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useMyProjects, useProjects } from "../../hooks/useProjects";
|
||||
import { useProjectTasks } from "../../hooks/useTasks";
|
||||
|
||||
type BreadcrumbMatch = {
|
||||
match: { params: { slug?: string; taskSlug?: string; nameIntern?: string } };
|
||||
};
|
||||
|
||||
const unslugify = (slug = '') =>
|
||||
slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
const ProjectBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const { projects } = useProjects();
|
||||
const slug = match.params.slug;
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
return <>{project?.project_name ?? unslugify(slug)}</>;
|
||||
};
|
||||
|
||||
const TaskBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const {slug, taskSlug} = match.params;
|
||||
|
||||
const { projects } = useMyProjects();
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
|
||||
const { tasks } = useProjectTasks(project?.id ?? 0);
|
||||
const task = tasks?.find((t) => t.slug === taskSlug);
|
||||
return <>{task?.title ?? unslugify(taskSlug)}</>;
|
||||
};
|
||||
|
||||
// Breadcrumb untuk halaman detail sertifikat (menggunakan nama project dari slug)
|
||||
const CertBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const { projects } = useProjects();
|
||||
const slug = match.params.slug;
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
return <>{project?.project_name ?? unslugify(slug)}</>;
|
||||
};
|
||||
|
||||
// Breadcrumb untuk halaman submission intern (unslugify nameIntern)
|
||||
const NameInternBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
return <>{unslugify(match.params.nameIntern)}</>;
|
||||
};
|
||||
|
||||
const breadcrumbRoutes = [
|
||||
// --- AREA INTERN ---
|
||||
{ path: '/intern', breadcrumb: 'Home' },
|
||||
{ path: '/intern/projects', breadcrumb: 'Projects' },
|
||||
{ path: '/intern/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/intern/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/intern/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/intern/certificates/:slug', breadcrumb: CertBreadcrumb },
|
||||
{ path: '/intern/faq', breadcrumb: 'FAQ' },
|
||||
{ path: '/intern/notifications', breadcrumb: 'Notifications' },
|
||||
{ path: '/intern/settings', breadcrumb: 'Settings' },
|
||||
|
||||
// --- AREA MENTOR / ADMIN ---
|
||||
{ path: '/mentor', breadcrumb: 'Home' },
|
||||
{ path: '/mentor/projects', breadcrumb: 'List Projects' },
|
||||
{ path: '/mentor/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug/:nameIntern', breadcrumb: NameInternBreadcrumb },
|
||||
{ path: '/mentor/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/mentor/certificates/:slug', breadcrumb: CertBreadcrumb },
|
||||
{ path: '/mentor/certificates/:slug/result', breadcrumb: 'Result' },
|
||||
{ path: '/mentor/intern', breadcrumb: 'List Intern' },
|
||||
{ path: '/mentor/faq', breadcrumb: 'FAQ' },
|
||||
{ path: '/mentor/notifications', breadcrumb: 'Notifications' },
|
||||
{ path: '/mentor/settings', breadcrumb: 'Settings' },
|
||||
];
|
||||
|
||||
export default function Breadcrumbs(){
|
||||
const breadcrumbs = useBreadcrumbs(breadcrumbRoutes);
|
||||
const location = useLocation();
|
||||
|
||||
const rootPaths = ['/intern', '/mentor'];
|
||||
const isOnRootPath = rootPaths.includes(location.pathname);
|
||||
const fixbreadcumbs = breadcrumbs.filter(({ match }) => {
|
||||
if (match.pathname === '/') return false;
|
||||
if (rootPaths.includes(match.pathname) && !isOnRootPath) return false;
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
<nav className="text-[15px] mb-3">
|
||||
{fixbreadcumbs.map(({ match, breadcrumb }) => {
|
||||
const isLast = match.pathname === location.pathname;
|
||||
|
||||
return (
|
||||
<span key={match.pathname} className="inline-flex items-center">
|
||||
{isLast ? (
|
||||
<span className="font-bold text-red-800">
|
||||
{breadcrumb}
|
||||
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Link to={match.pathname} className="text-gray-800">
|
||||
{breadcrumb}
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4 mx-1.5"/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import useBreadcrumbs from "use-react-router-breadcrumbs";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useMyProjects, useProjects } from "../../hooks/useProjects";
|
||||
import { useProjectTasks } from "../../hooks/useTasks";
|
||||
|
||||
type BreadcrumbMatch = {
|
||||
match: { params: { slug?: string; taskSlug?: string; nameIntern?: string } };
|
||||
};
|
||||
|
||||
const unslugify = (slug = '') =>
|
||||
slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
const ProjectBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const { projects } = useProjects();
|
||||
const slug = match.params.slug;
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
return <>{project?.project_name ?? unslugify(slug)}</>;
|
||||
};
|
||||
|
||||
const TaskBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const {slug, taskSlug} = match.params;
|
||||
|
||||
const { projects } = useMyProjects();
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
|
||||
const { tasks } = useProjectTasks(project?.id ?? 0);
|
||||
const task = tasks?.find((t) => t.slug === taskSlug);
|
||||
return <>{task?.title ?? unslugify(taskSlug)}</>;
|
||||
};
|
||||
|
||||
const CertBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
const { projects } = useProjects();
|
||||
const slug = match.params.slug;
|
||||
const project = projects?.find((p) => p.slug === slug);
|
||||
return <>{project?.project_name ?? unslugify(slug)}</>;
|
||||
};
|
||||
|
||||
const NameInternBreadcrumb = ({ match }: BreadcrumbMatch) => {
|
||||
return <>{unslugify(match.params.nameIntern)}</>;
|
||||
};
|
||||
|
||||
const breadcrumbRoutes = [
|
||||
// --- AREA INTERN ---
|
||||
{ path: '/intern', breadcrumb: 'Home' },
|
||||
{ path: '/intern/projects', breadcrumb: 'Projects' },
|
||||
{ path: '/intern/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/intern/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/intern/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/intern/certificates/:slug', breadcrumb: CertBreadcrumb },
|
||||
{ path: '/intern/faq', breadcrumb: 'FAQ' },
|
||||
{ path: '/intern/notifications', breadcrumb: 'Notifications' },
|
||||
{ path: '/intern/settings', breadcrumb: 'Settings' },
|
||||
|
||||
// --- AREA MENTOR / ADMIN ---
|
||||
{ path: '/mentor', breadcrumb: 'Home' },
|
||||
{ path: '/mentor/projects', breadcrumb: 'List Projects' },
|
||||
{ path: '/mentor/projects/:slug', breadcrumb: ProjectBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug', breadcrumb: TaskBreadcrumb },
|
||||
{ path: '/mentor/projects/:slug/:taskSlug/:nameIntern', breadcrumb: NameInternBreadcrumb },
|
||||
{ path: '/mentor/certificates', breadcrumb: 'Certificates' },
|
||||
{ path: '/mentor/certificates/:slug', breadcrumb: CertBreadcrumb },
|
||||
{ path: '/mentor/certificates/:slug/result', breadcrumb: 'Result' },
|
||||
{ path: '/mentor/intern', breadcrumb: 'List Intern' },
|
||||
{ path: '/mentor/faq', breadcrumb: 'FAQ' },
|
||||
{ path: '/mentor/notifications', breadcrumb: 'Notifications' },
|
||||
{ path: '/mentor/settings', breadcrumb: 'Settings' },
|
||||
];
|
||||
|
||||
export default function Breadcrumbs(){
|
||||
const breadcrumbs = useBreadcrumbs(breadcrumbRoutes);
|
||||
const location = useLocation();
|
||||
|
||||
const rootPaths = ['/intern', '/mentor'];
|
||||
const isOnRootPath = rootPaths.includes(location.pathname);
|
||||
const fixbreadcumbs = breadcrumbs.filter(({ match }) => {
|
||||
if (match.pathname === '/') return false;
|
||||
if (rootPaths.includes(match.pathname) && !isOnRootPath) return false;
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
<nav className="text-[15px] mb-3">
|
||||
{fixbreadcumbs.map(({ match, breadcrumb }) => {
|
||||
const isLast = match.pathname === location.pathname;
|
||||
|
||||
return (
|
||||
<span key={match.pathname} className="inline-flex items-center">
|
||||
{isLast ? (
|
||||
<span className="font-bold text-red-800">
|
||||
{breadcrumb}
|
||||
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<Link to={match.pathname} className="text-gray-800">
|
||||
{breadcrumb}
|
||||
</Link>
|
||||
<ChevronRight className="w-4 h-4 mx-1.5"/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,79 +1,78 @@
|
||||
import api from "../../lib/api";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface FAQ {
|
||||
id: number;
|
||||
pertanyaan: string;
|
||||
jawaban: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Skeleton item that mirrors the real FAQ card shape
|
||||
function FAQSkeleton() {
|
||||
return (
|
||||
<div className="relative shadow-md py-5 px-10 rounded-md bg-white border-l-6 border-gray-200 animate-pulse">
|
||||
{/* Question bar */}
|
||||
<div className="h-5 bg-gray-200 rounded w-2/3 mb-3" />
|
||||
{/* Answer bars */}
|
||||
<div className="pl-8 space-y-2">
|
||||
<div className="h-3 bg-gray-200 rounded w-full" />
|
||||
<div className="h-3 bg-gray-200 rounded w-4/5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FAQPage() {
|
||||
const [faq, setFaq] = useState<FAQ[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
api.get("/faq-api/get")
|
||||
.then((res) => {
|
||||
setFaq(res.data.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal fetch FAQ:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10">
|
||||
<h1 className="page-title">FAQ</h1>
|
||||
<p className="page-title-desc">
|
||||
Quick answers to common questions about the Internify LMS platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQ Lists */}
|
||||
{loading ? (
|
||||
<div className="space-y-8 max-w-auto">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<FAQSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8 max-w-auto">
|
||||
{faq.map((item) => (
|
||||
<div key={item.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white border-l-6 border-red hover:shadow-xl">
|
||||
|
||||
{/* Item Question */}
|
||||
<h2 className="text-[17px] font-bold text-red tracking-tight leading-snug mb-2">
|
||||
{item.pertanyaan}
|
||||
</h2>
|
||||
|
||||
<p className="text-[14px] pl-8 text-redfont-light leading-relaxed tracking-normal">
|
||||
{item.jawaban}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import api from "../../lib/api";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface FAQ {
|
||||
id: number;
|
||||
pertanyaan: string;
|
||||
jawaban: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
function FAQSkeleton() {
|
||||
return (
|
||||
<div className="relative shadow-md py-5 px-10 rounded-md bg-white border-l-6 border-gray-200 animate-pulse">
|
||||
{/* Question bar */}
|
||||
<div className="h-5 bg-gray-200 rounded w-2/3 mb-3" />
|
||||
{/* Answer bars */}
|
||||
<div className="pl-8 space-y-2">
|
||||
<div className="h-3 bg-gray-200 rounded w-full" />
|
||||
<div className="h-3 bg-gray-200 rounded w-4/5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FAQPage() {
|
||||
const [faq, setFaq] = useState<FAQ[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
api.get("/faq-api/get")
|
||||
.then((res) => {
|
||||
setFaq(res.data.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal fetch FAQ:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10">
|
||||
<h1 className="page-title">FAQ</h1>
|
||||
<p className="page-title-desc">
|
||||
Quick answers to common questions about the Internify LMS platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQ Lists */}
|
||||
{loading ? (
|
||||
<div className="space-y-8 max-w-auto">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<FAQSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8 max-w-auto">
|
||||
{faq.map((item) => (
|
||||
<div key={item.id} className="relative shadow-md group py-5 px-10 rounded-md bg-white border-l-6 border-red hover:shadow-xl">
|
||||
|
||||
{/* Item Question */}
|
||||
<h2 className="text-[17px] font-bold text-red tracking-tight leading-snug mb-2">
|
||||
{item.pertanyaan}
|
||||
</h2>
|
||||
|
||||
<p className="text-[14px] pl-8 text-redfont-light leading-relaxed tracking-normal">
|
||||
{item.jawaban}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,9 +58,9 @@ export default function InternifyLogin() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col justify-between items-center py-12 px-4 font-sans selection:bg-red-foreground">
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<div className="text-center mb-12">
|
||||
<div className="min-h-screen bg-background flex flex-col justify-between items-center py-6 px-4 selection:bg-red-foreground">
|
||||
<div className="w-full flex flex-col items-center mb-8">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center gap-2 font-bold text-2xl tracking-tight text-black">
|
||||
<img src={logo} alt="Logo" />
|
||||
</div>
|
||||
@@ -129,9 +129,9 @@ export default function InternifyLogin() {
|
||||
</form>
|
||||
{/* Forgot Password */}
|
||||
<div className="text-center mt-6">
|
||||
<a href="#" className="text-xs font-semibold text-red-600 hover:underline">
|
||||
<span className="text-xs font-semibold text-red-600">
|
||||
Forgot Password?
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Divider & Support */}
|
||||
@@ -144,7 +144,7 @@ export default function InternifyLogin() {
|
||||
<div className="flex-grow border-t border-card-outline"></div>
|
||||
</div>
|
||||
|
||||
<button className="w-full mt-2 flex items-center justify-center gap-2 border border-card-outline rounded-full py-1.5 text-xs text-font-shade hover:bg-gray-50 transition-colors font-medium">
|
||||
<button className="w-full mt-2 flex items-center justify-center gap-2 border border-card-outline rounded-full py-1.5 text-xs text-font-shade font-medium">
|
||||
<CircleQuestionMark className="w-5 h-5" />
|
||||
Frequently Asked Questions
|
||||
</button>
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ProjectsPage() {
|
||||
<h1 className="page-title-desc">View assigned projects and track their progress.</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-stretch">
|
||||
<div className="flex flex-wrap gap-3 items-stretch">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function MentorProjectsPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-stretch">{renderContent()}</div>
|
||||
<div className="flex flex-wrap gap-3 items-stretch">{renderContent()}</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<CreateProjectModal
|
||||
|
||||
@@ -216,7 +216,7 @@ export default function CertificateDetail() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* ── Project Scope ── */}
|
||||
{/* Project Scope */}
|
||||
<div className="border border-card-outline rounded-xl p-5">
|
||||
<p className="text-[13px] font-bold text-gray-400 uppercase tracking-widest mb-3">
|
||||
Project Scope
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function CertificateList() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Loading Skeleton — struktur sama persis dengan body */}
|
||||
{/* Loading Skeleton - struktur sama persis dengan body */}
|
||||
{loading && (
|
||||
<div>
|
||||
<div className="h-4 w-32 bg-gray-200 rounded mb-4 animate-pulse" />
|
||||
@@ -73,7 +73,7 @@ export default function CertificateList() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section: List Projects — hanya tampil kalau ada project */}
|
||||
{/* Section: List Projects - hanya tampil kalau ada project */}
|
||||
{hasProjects && (
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-font-shade mb-4">List Projects</h2>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { FileText } from 'lucide-react';
|
||||
export default function MentorForumTab({ project }: { project: Project }) {
|
||||
const { tasks, loading, error } = useProjectTasks(project.id);
|
||||
|
||||
// Skeleton meniru layout final: kolom timeline 1/5 + list task card 4/5
|
||||
const renderSkeleton = () => (
|
||||
<div className="flex flex-col gap-7 md:flex-row md:items-start mt-5 animate-pulse">
|
||||
<div className="w-full md:w-1/5 bg-white rounded-xl p-5 border border-box-border shrink-0">
|
||||
@@ -29,14 +28,12 @@ export default function MentorForumTab({ project }: { project: Project }) {
|
||||
</div>
|
||||
);
|
||||
|
||||
// Empty hanya valid setelah loading selesai & tidak error
|
||||
const isEmpty = !loading && !error && tasks.length === 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeroProject title={project.project_name} description={project.description} />
|
||||
|
||||
{/* Kondisi eksklusif: hanya satu state tampil dalam satu waktu */}
|
||||
{loading ? (
|
||||
renderSkeleton()
|
||||
) : error ? (
|
||||
|
||||
Reference in New Issue
Block a user