feat(dimentorin): audit isi halaman & layout + fix halaman kosong
- Homepage testimonial: hardcode Array.from({length:2}) duplikat Riko x2 -> 4 testimonial berbeda (Riko 4.8, Salsa 5.0, Dimas 4.9, Nadia 4.7)
- Fix typo 'dellow devs' -> 'fellow devs', 'Frequently Ask Question' -> 'Frequently Asked Questions'
- FAQ diperluas dari 3 -> 5 pertanyaan (payment, materi gratis)
- Buat halaman 404 brand IMPHNEN (Quest Tidak Ditemukan!) dgn Header+Footer di (public)/404.tsx
- Fix middleware: 404/route tak dikenal tidak redirect ke login (hanya prefix admin /dashboard,/users,/roles,/permissions yg protected)
- my-sessions & mentor-dashboard: guard SessionToken -> CTA 'Login dulu, Senpai!' (sebelumnya loading forever karena API 401 tanpa token)
This commit is contained in:
@@ -5,9 +5,11 @@ import { motion, useInView, Variants } from 'framer-motion'
|
||||
import { cn, For } from '@imphnen-frontend-service/utils'
|
||||
|
||||
const FAQ_DATA: AccordionOptions[] = [
|
||||
{ title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bisa memilih mentor yang sesuai dengan kebutuhan dan level skill kamu.' },
|
||||
{ title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas untuk mempercepat progres belajarnya.' },
|
||||
{ title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih dari sekadar kursus-kursus AI guide dan mentor berpengalaman, kamu mendapatkan pengalaman belajar yang jauh lebih personal dan efektif.' },
|
||||
{ title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bebas memilih mentor yang sesuai dengan kebutuhan, topik, dan level skill kamu. Setiap mentor punya profil, expertise, dan jadwal yang bisa kamu lihat sebelum booking.' },
|
||||
{ title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas. Tidak tahu harus mulai dari mana? Tanya AI Agent dulu untuk arahan belajar, lalu booking mentor untuk pendampingan yang lebih dalam.' },
|
||||
{ title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih fleksibel dari kursus pada umumnya. Rasa seperti punya mentor pribadi yang menyesuaikan pace belajarmu, plus AI Agent yang siap membantu kapan pun kamu macet.' },
|
||||
{ title: 'Bagaimana sistem pembayarannya?', description: 'Kamu bayar per sesi mentoring yang sudah di-booking. Pembayaran diproses via Midtrans (Virtual Account / QRIS), dan sesi otomatis terkonfirmasi begitu pembayaran lunas.' },
|
||||
{ title: 'Apakah ada materi yang bisa dipelajari gratis?', description: 'Ada! Halaman Materi berisi artikel dari para mentor yang bisa kamu baca kapan saja, dan AI Agent AI bisa menjawab pertanyaanmu berdasar materi tersebut tanpa biaya tambahan.' },
|
||||
]
|
||||
|
||||
export const FAQSection: FC = () => {
|
||||
@@ -72,7 +74,7 @@ export const FAQSection: FC = () => {
|
||||
variant="bordered"
|
||||
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
|
||||
>
|
||||
Frequently Ask Question
|
||||
Frequently Asked Questions
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
|
||||
@@ -8,6 +8,33 @@ export const TestimonialSection: FC = () => {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: 'Riko, Junior Web Developer',
|
||||
quote:
|
||||
'Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin!',
|
||||
rating: '4.8',
|
||||
},
|
||||
{
|
||||
name: 'Salsa, Data Analyst',
|
||||
quote:
|
||||
'AI Agent-nya ngebantu banget pas stuck di konsep SQL yang rumit. Jawabannya selalu lengkap sama sumber materinya. Recommended!',
|
||||
rating: '5.0',
|
||||
},
|
||||
{
|
||||
name: 'Dimas, Mahasiswa Sistem Informasi',
|
||||
quote:
|
||||
'Awalnya takut tanya dosen, sekarang tinggal chat AI agent atau booking mentor. Progress belajarku naik drastis dalam 2 bulan!',
|
||||
rating: '4.9',
|
||||
},
|
||||
{
|
||||
name: 'Nadia, UI/UX Designer',
|
||||
quote:
|
||||
'Sensei-nya sabar banget jelasin dari dasar. Materi Rust & Tokio di sini juga disusun rapi, jadi gampang diikuti step by step.',
|
||||
rating: '4.7',
|
||||
},
|
||||
]
|
||||
|
||||
const containerVariants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
@@ -57,7 +84,7 @@ export const TestimonialSection: FC = () => {
|
||||
viewport={{ once: true }}
|
||||
className="my-6 text-lg font-semibold mx-auto text-center text-primary-500 md:text-2xl lg:my-10 lg:text-4xl"
|
||||
>
|
||||
Words from our dellow devs
|
||||
Words from our fellow devs
|
||||
</motion.h1>
|
||||
|
||||
<div className="scrollbar-hide w-full mx-auto max-w-7xl px-8 overflow-auto md:px-[60px] xl:px-0">
|
||||
@@ -67,8 +94,8 @@ export const TestimonialSection: FC = () => {
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
>
|
||||
<For data={Array.from({ length: 2 })}>
|
||||
{(_, index) => (
|
||||
<For data={testimonials}>
|
||||
{(testimonial, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={childVariants}
|
||||
@@ -83,10 +110,10 @@ export const TestimonialSection: FC = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="mb-3 font-semibold text-primary-500 md:text-lg lg:text-2xl">
|
||||
Riko, Junior Web Developer
|
||||
{testimonial.name}
|
||||
</h3>
|
||||
<p className="mb-6 text-[10px] text-neutral-500 font-medium md:text-sm lg:text-base">
|
||||
Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin!
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<motion.div className="w-max" whileHover={{ scale: 1.1, rotate: -3 }}>
|
||||
<Button
|
||||
@@ -97,7 +124,7 @@ export const TestimonialSection: FC = () => {
|
||||
>
|
||||
<StarFilled className="md:text-lg" />
|
||||
<span>
|
||||
<span className="text-sm font-medium md:text-lg md:font-semibold lg:text-xl">4.8</span>
|
||||
<span className="text-sm font-medium md:text-lg md:font-semibold lg:text-xl">{testimonial.rating}</span>
|
||||
<span className="text-[10px] text-primary-300 md:text-sm md:font-medium lg:text-base">/5.0</span>
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="relative min-h-[70vh] flex items-center justify-center overflow-hidden bg-primary-50 px-6">
|
||||
{/* Decorative circles */}
|
||||
<div className="pointer-events-none absolute -top-24 -right-24 size-72 rounded-full bg-primary-200 md:size-96" />
|
||||
<div className="pointer-events-none absolute -bottom-28 -left-24 size-72 rounded-full bg-primary-100 md:size-96" />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-2xl text-center">
|
||||
<p className="text-[46px] font-bold leading-none text-primary-500 md:text-[96px]">
|
||||
404
|
||||
</p>
|
||||
<h1 className="mt-4 text-[23px] font-semibold text-neutral-800 md:text-[29px]">
|
||||
Quest Tidak Ditemukan!
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-md text-[15px] text-neutral-500 md:text-[19px]">
|
||||
Sepertinya halaman ini pergi ke isekai tanpa kamu... atau alamatnya
|
||||
salah ketik. Jangan khawatir, pahlawan sejati selalu tahu jalan
|
||||
pulang! 🗡️✨
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<Link to="/">
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto px-8"
|
||||
>
|
||||
Kembali ke Home
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/mentoring">
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
variant="bordered"
|
||||
className="w-full sm:w-auto px-8"
|
||||
>
|
||||
Cari Mentor
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-[12px] text-neutral-400">
|
||||
Level up > 404 Not Found > Berhasil kembali ke jalur quest
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { For, Show, cn } from '@imphnen-frontend-service/utils';
|
||||
import { For, Show, cn, SessionToken } from '@imphnen-frontend-service/utils';
|
||||
import {
|
||||
useGetMentorMe,
|
||||
useGetMentorSessions,
|
||||
@@ -125,6 +125,26 @@ export const Components: FC = (): ReactElement => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
condition={SessionToken.get() != null}
|
||||
fallback={
|
||||
<div className="bg-white rounded-lg p-12 text-center shadow-sm border border-neutral-100">
|
||||
<p className="text-[46px] mb-2">🔐</p>
|
||||
<h2 className="text-xl font-semibold text-neutral-800 mb-2">
|
||||
Login dulu, Senpai!
|
||||
</h2>
|
||||
<p className="text-neutral-500 mb-6 max-w-md mx-auto">
|
||||
Dashboard mentor hanya untuk yang sudah terdaftar. Masuk untuk
|
||||
melihat permintaan sesi dan konfirmasi pembayaran.
|
||||
</p>
|
||||
<Link to="/auth/login">
|
||||
<Button variant="primary" size="lg">
|
||||
Login Sekarang
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
condition={!loadingMentor && !mentor}
|
||||
fallback={
|
||||
@@ -212,6 +232,7 @@ export const Components: FC = (): ReactElement => {
|
||||
</Link>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, ReactElement } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { For, Show, cn } from '@imphnen-frontend-service/utils';
|
||||
import { For, Show, cn, SessionToken } from '@imphnen-frontend-service/utils';
|
||||
import {
|
||||
useGetMySessions,
|
||||
useGetMyPayments,
|
||||
@@ -81,6 +81,10 @@ export const Components: FC = (): ReactElement => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
condition={SessionToken.get() != null}
|
||||
fallback={<AnonymousState />}
|
||||
>
|
||||
<Show
|
||||
condition={isLoading}
|
||||
fallback={
|
||||
@@ -183,9 +187,28 @@ export const Components: FC = (): ReactElement => {
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const AnonymousState: FC = (): ReactElement => (
|
||||
<div className="bg-white rounded-lg p-12 text-center shadow-sm border border-neutral-100">
|
||||
<p className="text-[46px] mb-2">🔐</p>
|
||||
<h2 className="text-xl font-semibold text-neutral-800 mb-2">
|
||||
Login dulu, Senpai!
|
||||
</h2>
|
||||
<p className="text-neutral-500 mb-6 max-w-md mx-auto">
|
||||
Sesi mentoring kamu aman tersimpan di akun. Masuk untuk melihat jadwal,
|
||||
status pembayaran, dan konfirmasi sesi.
|
||||
</p>
|
||||
<Link to="/auth/login">
|
||||
<Button variant="primary" size="lg">
|
||||
Login Sekarang
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Components;
|
||||
|
||||
@@ -82,6 +82,13 @@ const mappingPublicPrefixRoutes = [
|
||||
'/articles',
|
||||
]
|
||||
|
||||
const mappingProtectedPrefixRoutes = [
|
||||
'/dashboard',
|
||||
'/users',
|
||||
'/roles',
|
||||
'/permissions',
|
||||
]
|
||||
|
||||
//TODO : Fix this later
|
||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||
// const fallback = mappingRoutePermissions.find((route) =>
|
||||
@@ -111,21 +118,24 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!session) return redirect('/auth/login');
|
||||
|
||||
const matchedRoute = mappingRoutePermissions.find(
|
||||
(route) => route.path === pathname
|
||||
);
|
||||
|
||||
if (matchedRoute) {
|
||||
const hasPermission =
|
||||
!matchedRoute.permissions ||
|
||||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
|
||||
|
||||
if (!hasPermission) {
|
||||
return '/dashboard';
|
||||
// Route yang butuh autentikasi: prefix admin backoffice
|
||||
if (mappingProtectedPrefixRoutes.some((p) => pathname.startsWith(p))) {
|
||||
if (!session) return redirect('/auth/login');
|
||||
const matchedRoute = mappingRoutePermissions.find(
|
||||
(route) => route.path === pathname
|
||||
);
|
||||
if (matchedRoute) {
|
||||
const hasPermission =
|
||||
!matchedRoute.permissions ||
|
||||
matchedRoute.permissions.some((perm) =>
|
||||
userPermissions.includes(perm)
|
||||
);
|
||||
if (!hasPermission) return '/dashboard';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Route publik/halaman FE (materi, ai-agent, my-sessions, detail mentor,
|
||||
// 404 tak dikenal) tidak redirect — biarkan halaman-route menanganinya.
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user