pramudya-yb commited on
Commit ·
fe5ae84
1
Parent(s): cbfc761
Minimalize landing page and remove rogasper components
Browse files- README.md +1 -7
- apps/web/src/components/CommunityChannelList.tsx +0 -90
- apps/web/src/components/CommunityModal.tsx +0 -39
- apps/web/src/components/DonationModal.tsx +0 -59
- apps/web/src/components/routes/LandingPage.tsx +4 -217
- apps/web/src/lib/community-links.ts +0 -73
- apps/web/src/routes/__root.tsx +2 -5
- apps/web/src/routes/dashboard.tsx +4 -40
- apps/web/vite.config.ts +2 -11
README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
title: Pram API
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: green
|
|
@@ -278,12 +278,6 @@ Contributions are welcome! Before opening a PR:
|
|
| 278 |
|
| 279 |
For AI-related changes, see the **AI Generation Context** section in `AGENTS.md` — adding a new question format touches schemas, prompts, attempt normalization, and frontend constants.
|
| 280 |
|
| 281 |
-
## Support
|
| 282 |
-
|
| 283 |
-
If you find Pram helpful and want to support its ongoing development, consider buying the creator a coffee! Your support helps cover API and hosting costs. ☕
|
| 284 |
-
|
| 285 |
-
- **[Saweria](https://saweria.co/pram)** (Indonesian supporters)
|
| 286 |
-
- **[Ko-fi](https://ko-fi.com/pram)** (International)
|
| 287 |
|
| 288 |
## License
|
| 289 |
|
|
|
|
| 1 |
+
---
|
| 2 |
title: Pram API
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: green
|
|
|
|
| 278 |
|
| 279 |
For AI-related changes, see the **AI Generation Context** section in `AGENTS.md` — adding a new question format touches schemas, prompts, attempt normalization, and frontend constants.
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
## License
|
| 283 |
|
apps/web/src/components/CommunityChannelList.tsx
DELETED
|
@@ -1,90 +0,0 @@
|
|
| 1 |
-
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 2 |
-
import {
|
| 3 |
-
COMMUNITY_PLATFORM_META,
|
| 4 |
-
getCommunitySections,
|
| 5 |
-
type CommunityChannel,
|
| 6 |
-
type CommunityPlatform,
|
| 7 |
-
} from "@/lib/community-links";
|
| 8 |
-
|
| 9 |
-
function ChannelRow({
|
| 10 |
-
channel,
|
| 11 |
-
platform,
|
| 12 |
-
variant,
|
| 13 |
-
}: {
|
| 14 |
-
channel: CommunityChannel;
|
| 15 |
-
platform: CommunityPlatform;
|
| 16 |
-
variant: "modal" | "card";
|
| 17 |
-
}) {
|
| 18 |
-
const meta = COMMUNITY_PLATFORM_META[platform];
|
| 19 |
-
const baseClass =
|
| 20 |
-
variant === "modal"
|
| 21 |
-
? `w-full flex items-start gap-3 py-3.5 px-4 rounded-[var(--radius-lg)] font-semibold transition-all text-[var(--clay-black)] border-2 clay-hover ${meta?.linkClass ?? ""}`
|
| 22 |
-
: `flex items-start gap-3 rounded-[var(--radius-lg)] border px-4 py-3 transition-colors ${meta?.linkClass ?? "border-[var(--oat-border)] bg-[var(--warm-cream)]/60"}`;
|
| 23 |
-
|
| 24 |
-
const iconClass = meta?.iconClass ?? "text-[var(--matcha-700)]";
|
| 25 |
-
|
| 26 |
-
if (!channel.href) {
|
| 27 |
-
return (
|
| 28 |
-
<div
|
| 29 |
-
className={`${baseClass} opacity-70 cursor-not-allowed border-dashed`}
|
| 30 |
-
aria-disabled="true"
|
| 31 |
-
>
|
| 32 |
-
<MaterialIcon name={channel.icon} className={`text-2xl shrink-0 mt-0.5 ${iconClass}`} />
|
| 33 |
-
<span className="text-left flex-1">
|
| 34 |
-
<span className="flex items-center gap-2 flex-wrap">
|
| 35 |
-
<span className="block font-bold">{channel.name}</span>
|
| 36 |
-
<span className="text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full bg-[var(--oat-light)] text-[var(--warm-charcoal)]">
|
| 37 |
-
Segera hadir
|
| 38 |
-
</span>
|
| 39 |
-
</span>
|
| 40 |
-
<span className="block text-sm font-normal text-[var(--warm-charcoal)] mt-0.5">
|
| 41 |
-
{channel.description}
|
| 42 |
-
</span>
|
| 43 |
-
</span>
|
| 44 |
-
</div>
|
| 45 |
-
);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<a
|
| 50 |
-
href={channel.href}
|
| 51 |
-
target="_blank"
|
| 52 |
-
rel="noopener noreferrer"
|
| 53 |
-
className={baseClass}
|
| 54 |
-
>
|
| 55 |
-
<MaterialIcon name={channel.icon} className={`text-2xl shrink-0 mt-0.5 ${iconClass}`} />
|
| 56 |
-
<span className="text-left">
|
| 57 |
-
<span className={`block font-bold ${variant === "card" ? "text-sm" : ""}`}>
|
| 58 |
-
{channel.name}
|
| 59 |
-
</span>
|
| 60 |
-
<span
|
| 61 |
-
className={`block font-normal text-[var(--warm-charcoal)] mt-0.5 ${variant === "card" ? "text-xs" : "text-sm"}`}
|
| 62 |
-
>
|
| 63 |
-
{channel.description}
|
| 64 |
-
</span>
|
| 65 |
-
</span>
|
| 66 |
-
</a>
|
| 67 |
-
);
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
export function CommunityChannelList({ variant }: { variant: "modal" | "card" }) {
|
| 71 |
-
const sections = getCommunitySections();
|
| 72 |
-
|
| 73 |
-
return (
|
| 74 |
-
<div className={variant === "modal" ? "flex flex-col gap-5" : "space-y-5"}>
|
| 75 |
-
{sections.map(({ platform, meta, channels }) => (
|
| 76 |
-
<section key={platform} className="space-y-2">
|
| 77 |
-
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-[var(--warm-charcoal)]">
|
| 78 |
-
<MaterialIcon name={meta.icon} className={`text-base ${meta.iconClass}`} />
|
| 79 |
-
{meta.label}
|
| 80 |
-
</h3>
|
| 81 |
-
<div className={variant === "modal" ? "flex flex-col gap-2" : "space-y-2"}>
|
| 82 |
-
{channels.map((channel) => (
|
| 83 |
-
<ChannelRow key={channel.id} channel={channel} platform={platform} variant={variant} />
|
| 84 |
-
))}
|
| 85 |
-
</div>
|
| 86 |
-
</section>
|
| 87 |
-
))}
|
| 88 |
-
</div>
|
| 89 |
-
);
|
| 90 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apps/web/src/components/CommunityModal.tsx
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog";
|
| 2 |
-
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 3 |
-
import { CommunityChannelList } from "@/components/CommunityChannelList";
|
| 4 |
-
|
| 5 |
-
export function CommunityModal({
|
| 6 |
-
isOpen,
|
| 7 |
-
onOpenChange,
|
| 8 |
-
}: {
|
| 9 |
-
isOpen: boolean;
|
| 10 |
-
onOpenChange: (open: boolean) => void;
|
| 11 |
-
}) {
|
| 12 |
-
return (
|
| 13 |
-
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
| 14 |
-
<DialogContent className="sm:max-w-[440px] p-6 rounded-[var(--radius-xl)] bg-[var(--pure-white)] border-2 border-[var(--oat-border)]">
|
| 15 |
-
<DialogHeader>
|
| 16 |
-
<div className="mx-auto w-16 h-16 bg-[var(--matcha-300)]/30 rounded-full flex items-center justify-center mb-2">
|
| 17 |
-
<MaterialIcon name="forum" className="text-4xl text-[var(--matcha-800)]" />
|
| 18 |
-
</div>
|
| 19 |
-
<DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
|
| 20 |
-
Gabung Komunitas Pram
|
| 21 |
-
</DialogTitle>
|
| 22 |
-
<DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
|
| 23 |
-
Belajar bahasa, lapor bug, atau ikut ngembangin Pram — pilih channel yang paling pas.
|
| 24 |
-
</DialogDescription>
|
| 25 |
-
</DialogHeader>
|
| 26 |
-
<div className="mt-6">
|
| 27 |
-
<CommunityChannelList variant="modal" />
|
| 28 |
-
<button
|
| 29 |
-
type="button"
|
| 30 |
-
onClick={() => onOpenChange(false)}
|
| 31 |
-
className="mt-5 w-full text-sm text-[var(--warm-silver)] hover:text-[var(--clay-black)] font-semibold transition-colors"
|
| 32 |
-
>
|
| 33 |
-
Nanti Saja
|
| 34 |
-
</button>
|
| 35 |
-
</div>
|
| 36 |
-
</DialogContent>
|
| 37 |
-
</Dialog>
|
| 38 |
-
);
|
| 39 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apps/web/src/components/DonationModal.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@pram/ui/components/dialog";
|
| 2 |
-
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 3 |
-
|
| 4 |
-
export function DonationModal({
|
| 5 |
-
isOpen,
|
| 6 |
-
onOpenChange,
|
| 7 |
-
triggerAction
|
| 8 |
-
}: {
|
| 9 |
-
isOpen: boolean;
|
| 10 |
-
onOpenChange: (open: boolean) => void;
|
| 11 |
-
triggerAction: "exam" | "generate" | null;
|
| 12 |
-
}) {
|
| 13 |
-
return (
|
| 14 |
-
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
| 15 |
-
<DialogContent className="sm:max-w-[425px] p-6 rounded-[var(--radius-xl)] bg-[var(--pure-white)] border-2 border-[var(--oat-border)]">
|
| 16 |
-
<DialogHeader>
|
| 17 |
-
<div className="mx-auto w-16 h-16 bg-[var(--pomegranate-400)]/10 rounded-full flex items-center justify-center mb-2">
|
| 18 |
-
<MaterialIcon name="favorite" className="text-4xl text-[var(--pomegranate-400)]" />
|
| 19 |
-
</div>
|
| 20 |
-
<DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
|
| 21 |
-
Dukung Pram!
|
| 22 |
-
</DialogTitle>
|
| 23 |
-
<DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
|
| 24 |
-
Selamat, kamu baru saja menyelesaikan {triggerAction === "exam" ? "latihan soal" : "generate soal AI"}! 🎉
|
| 25 |
-
<br/><br/>
|
| 26 |
-
Jika Pram membantumu, dukung kami untuk menutupi biaya server & mengembangkan fitur baru dengan mentraktir kopi. ☕
|
| 27 |
-
</DialogDescription>
|
| 28 |
-
</DialogHeader>
|
| 29 |
-
<div className="flex flex-col gap-3 mt-6">
|
| 30 |
-
<a
|
| 31 |
-
href="https://saweria.co/pram"
|
| 32 |
-
target="_blank"
|
| 33 |
-
rel="noopener noreferrer"
|
| 34 |
-
className="w-full flex items-center justify-center gap-2 py-3.5 px-4 rounded-[var(--radius-lg)] font-bold transition-all bg-[#F3B63A] text-black hover:brightness-95 clay-shadow clay-hover"
|
| 35 |
-
>
|
| 36 |
-
<MaterialIcon name="favorite" className="text-xl" />
|
| 37 |
-
Support via Saweria
|
| 38 |
-
</a>
|
| 39 |
-
<a
|
| 40 |
-
href="https://ko-fi.com/pram"
|
| 41 |
-
target="_blank"
|
| 42 |
-
rel="noopener noreferrer"
|
| 43 |
-
className="w-full flex items-center justify-center gap-2 py-3.5 px-4 rounded-[var(--radius-lg)] font-bold transition-all bg-[#13C3FF] text-white hover:brightness-95 clay-shadow clay-hover"
|
| 44 |
-
>
|
| 45 |
-
<MaterialIcon name="local_cafe" className="text-xl" />
|
| 46 |
-
Support via Ko-fi
|
| 47 |
-
</a>
|
| 48 |
-
<button
|
| 49 |
-
onClick={() => onOpenChange(false)}
|
| 50 |
-
className="mt-3 text-sm text-[var(--warm-silver)] hover:text-[var(--clay-black)] font-semibold transition-colors"
|
| 51 |
-
>
|
| 52 |
-
Mungkin Nanti Saja
|
| 53 |
-
</button>
|
| 54 |
-
</div>
|
| 55 |
-
</DialogContent>
|
| 56 |
-
</Dialog>
|
| 57 |
-
);
|
| 58 |
-
}
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apps/web/src/components/routes/LandingPage.tsx
CHANGED
|
@@ -2,47 +2,13 @@ import { Link } from "@tanstack/react-router";
|
|
| 2 |
import { Button } from "@pram/ui/components/button";
|
| 3 |
import { Card, CardContent } from "@pram/ui/components/card";
|
| 4 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 5 |
-
import { useState } from "react";
|
| 6 |
-
import { CommunityModal } from "@/components/CommunityModal";
|
| 7 |
|
| 8 |
-
function FaqItem({ question, answer, isDark }: { question: string; answer: string; isDark?: boolean }) {
|
| 9 |
-
const [isOpen, setIsOpen] = useState(false);
|
| 10 |
|
| 11 |
-
return (
|
| 12 |
-
<div className={`border-b-2 py-6 ${isDark ? 'border-[var(--ube-300)]/30' : 'border-[var(--oat-border)]'} last:border-b-0`}>
|
| 13 |
-
<button
|
| 14 |
-
type="button"
|
| 15 |
-
className="flex w-full items-center justify-between text-left focus:outline-none group"
|
| 16 |
-
onClick={() => setIsOpen(!isOpen)}
|
| 17 |
-
aria-expanded={isOpen}
|
| 18 |
-
>
|
| 19 |
-
<span className={`font-headline font-semibold text-2xl tracking-[-0.64px] transition-colors ${isDark ? 'text-[var(--pure-white)] group-hover:text-[var(--ube-300)]' : 'text-[var(--clay-black)] group-hover:text-[var(--matcha-700)]'}`}>
|
| 20 |
-
{question}
|
| 21 |
-
</span>
|
| 22 |
-
<div className={`w-10 h-10 rounded-full border-2 flex items-center justify-center transition-transform duration-300 ${isOpen ? "rotate-180" : ""} ${isDark ? 'border-[var(--ube-300)]/50 text-[var(--pure-white)] group-hover:bg-[var(--ube-300)]/20' : 'border-[var(--oat-border)] text-[var(--clay-black)] group-hover:bg-[var(--oat-light)]'}`}>
|
| 23 |
-
<MaterialIcon
|
| 24 |
-
name="expand_more"
|
| 25 |
-
className="text-2xl"
|
| 26 |
-
/>
|
| 27 |
-
</div>
|
| 28 |
-
</button>
|
| 29 |
-
<div
|
| 30 |
-
className={`overflow-hidden transition-all duration-300 ease-in-out ${isOpen ? "max-h-96 mt-4 opacity-100" : "max-h-0 opacity-0"}`}
|
| 31 |
-
>
|
| 32 |
-
<p className={`text-lg leading-relaxed pr-12 ${isDark ? 'text-[var(--ube-300)]' : 'text-[var(--warm-charcoal)]'}`}>
|
| 33 |
-
{answer}
|
| 34 |
-
</p>
|
| 35 |
-
</div>
|
| 36 |
-
</div>
|
| 37 |
-
);
|
| 38 |
-
}
|
| 39 |
|
| 40 |
-
export function LandingPage() {
|
| 41 |
-
const [communityModalOpen, setCommunityModalOpen] = useState(false);
|
| 42 |
|
|
|
|
| 43 |
return (
|
| 44 |
<div className="min-h-screen bg-[var(--warm-cream)] flex flex-col font-sans selection:bg-[var(--matcha-300)] selection:text-[var(--clay-black)]">
|
| 45 |
-
<CommunityModal isOpen={communityModalOpen} onOpenChange={setCommunityModalOpen} />
|
| 46 |
{/* Navbar */}
|
| 47 |
<nav className="w-full px-6 py-4 md:px-12 lg:px-16 flex items-center justify-between max-w-7xl mx-auto z-50 sticky top-0 bg-[var(--warm-cream)] border-b-2 border-[var(--oat-border)]">
|
| 48 |
<div className="flex items-center gap-3">
|
|
@@ -50,16 +16,7 @@ export function LandingPage() {
|
|
| 50 |
<span className="font-headline font-semibold text-2xl tracking-[-0.64px] text-[var(--clay-black)] hidden sm:block">Pram</span>
|
| 51 |
</div>
|
| 52 |
<div className="flex items-center gap-2 sm:gap-4">
|
| 53 |
-
|
| 54 |
-
type="button"
|
| 55 |
-
onClick={() => setCommunityModalOpen(true)}
|
| 56 |
-
className="text-[var(--warm-charcoal)] hover:text-[var(--matcha-700)] transition-colors p-2 hidden md:flex items-center gap-2"
|
| 57 |
-
aria-label="Komunitas WhatsApp"
|
| 58 |
-
>
|
| 59 |
-
<MaterialIcon name="forum" className="text-xl" />
|
| 60 |
-
<span className="font-semibold text-base">Komunitas</span>
|
| 61 |
-
</button>
|
| 62 |
-
<div className="w-px h-6 bg-[var(--oat-border)] hidden sm:block mx-2"></div>
|
| 63 |
<Link to="/login">
|
| 64 |
<Button variant="ghost" className="text-[var(--clay-black)] font-semibold hover:bg-[var(--oat-light)] rounded-[12px] text-lg px-4 sm:px-6 h-12">
|
| 65 |
Masuk
|
|
@@ -73,9 +30,9 @@ export function LandingPage() {
|
|
| 73 |
</div>
|
| 74 |
</nav>
|
| 75 |
|
| 76 |
-
<main className="flex-1 flex flex-col items-center
|
| 77 |
{/* HUGE HERO SECTION */}
|
| 78 |
-
<section className="w-full px-6 md:px-12 lg:px-16
|
| 79 |
<div className="flex-1 text-center xl:text-left space-y-8 z-10 w-full max-w-[800px] xl:max-w-none">
|
| 80 |
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--matcha-300)] border-2 border-[var(--matcha-800)] text-[var(--clay-black)] uppercase-label shadow-sm mx-auto xl:mx-0 transform -rotate-2">
|
| 81 |
<MaterialIcon name="auto_awesome" className="text-sm" />
|
|
@@ -123,177 +80,7 @@ export function LandingPage() {
|
|
| 123 |
|
| 124 |
|
| 125 |
|
| 126 |
-
{/* Feature Cards Section */}
|
| 127 |
-
<section id="features" className="w-full bg-[var(--pure-white)] py-32 border-y-2 border-dashed border-[var(--oat-border)]">
|
| 128 |
-
<div className="max-w-7xl mx-auto px-6 md:px-12 lg:px-16">
|
| 129 |
-
<div className="text-center max-w-4xl mx-auto mb-24 space-y-6">
|
| 130 |
-
<h2 className="text-[50px] md:text-[60px] font-headline font-semibold text-[var(--clay-black)] tracking-[-2.4px] leading-tight">
|
| 131 |
-
Fitur Unggulan Pram
|
| 132 |
-
</h2>
|
| 133 |
-
<p className="text-2xl text-[var(--warm-charcoal)]">
|
| 134 |
-
Desain yang elegan, namun kokoh untuk mempercepat kesiapan Anda menghadapi ujian.
|
| 135 |
-
</p>
|
| 136 |
-
</div>
|
| 137 |
-
|
| 138 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 139 |
-
{/* Feature 1 */}
|
| 140 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 141 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 142 |
-
<img src="/generateai.png" alt="AI Generator" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 143 |
-
</div>
|
| 144 |
-
<CardContent className="p-6">
|
| 145 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 146 |
-
AI Generator
|
| 147 |
-
</h3>
|
| 148 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 149 |
-
Hasilkan soal latihan baru menggunakan AI. Tentukan topik, level kesulitan, dan format dalam hitungan detik.
|
| 150 |
-
</p>
|
| 151 |
-
</CardContent>
|
| 152 |
-
</Card>
|
| 153 |
-
|
| 154 |
-
{/* Feature 2 */}
|
| 155 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 156 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 157 |
-
<img src="/latihansoal.png" alt="Latihan Soal" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:-rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 158 |
-
</div>
|
| 159 |
-
<CardContent className="p-6">
|
| 160 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 161 |
-
Latihan Terfokus
|
| 162 |
-
</h3>
|
| 163 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 164 |
-
Akses ribuan soal latihan dari bank soal. Buat paket latihan Anda sendiri dan fokus pada area kelemahan.
|
| 165 |
-
</p>
|
| 166 |
-
</CardContent>
|
| 167 |
-
</Card>
|
| 168 |
-
|
| 169 |
-
{/* Feature 3 */}
|
| 170 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 171 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 172 |
-
<img src="/mocktest.png" alt="Mock Test" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 173 |
-
</div>
|
| 174 |
-
<CardContent className="p-6">
|
| 175 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 176 |
-
Simulasi Ujian
|
| 177 |
-
</h3>
|
| 178 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 179 |
-
Simulasikan suasana ujian sesungguhnya dengan batas waktu, antarmuka imersif, dan penilaian instan.
|
| 180 |
-
</p>
|
| 181 |
-
</CardContent>
|
| 182 |
-
</Card>
|
| 183 |
-
|
| 184 |
-
{/* Feature 4 */}
|
| 185 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 186 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 187 |
-
<img src="/progress.png" alt="Progress Tracking" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:-rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 188 |
-
</div>
|
| 189 |
-
<CardContent className="p-6">
|
| 190 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 191 |
-
Analitik Progres
|
| 192 |
-
</h3>
|
| 193 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 194 |
-
Lacak perkembangan nilai Anda dari waktu ke waktu. Analisis mendalam untuk setiap bagian tes bahasa.
|
| 195 |
-
</p>
|
| 196 |
-
</CardContent>
|
| 197 |
-
</Card>
|
| 198 |
-
|
| 199 |
-
{/* Feature 5 */}
|
| 200 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 201 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 202 |
-
<img src="/vocabulary.png" alt="Vocabulary" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 203 |
-
</div>
|
| 204 |
-
<CardContent className="p-6">
|
| 205 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 206 |
-
Kosakata
|
| 207 |
-
</h3>
|
| 208 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 209 |
-
Perkaya kosakata Anda dengan metode cerdas dan pengulangan berkala yang dioptimalkan.
|
| 210 |
-
</p>
|
| 211 |
-
</CardContent>
|
| 212 |
-
</Card>
|
| 213 |
-
|
| 214 |
-
{/* Feature 6 */}
|
| 215 |
-
<Card className="clay-shadow clay-hover bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[24px] overflow-hidden group p-4">
|
| 216 |
-
<div className="h-56 w-full flex items-center justify-center p-2">
|
| 217 |
-
<img src="/diskusi.png" alt="Diskusi" className="h-full w-auto object-contain transform group-hover:scale-110 group-hover:-rotate-3 transition-transform duration-500 drop-shadow-lg" />
|
| 218 |
-
</div>
|
| 219 |
-
<CardContent className="p-6">
|
| 220 |
-
<h3 className="font-headline text-[32px] font-semibold text-[var(--clay-black)] tracking-[-0.64px] mb-3 leading-tight">
|
| 221 |
-
Forum Diskusi
|
| 222 |
-
</h3>
|
| 223 |
-
<p className="text-[var(--warm-charcoal)] text-lg leading-relaxed">
|
| 224 |
-
Diskusikan soal-soal sulit dengan komunitas pembelajar lainnya dan dapatkan penjelasan ahli.
|
| 225 |
-
</p>
|
| 226 |
-
</CardContent>
|
| 227 |
-
</Card>
|
| 228 |
-
</div>
|
| 229 |
-
</div>
|
| 230 |
-
</section>
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
{/* SWATCH ROOM 1: Ube FAQ Section */}
|
| 235 |
-
<section className="w-full bg-[var(--ube-800)] py-32 rounded-t-[40px] mt-[-40px] z-10 relative shadow-[0_-10px_40px_rgba(0,0,0,0.1)] border-t-2 border-[var(--oat-border)]/20">
|
| 236 |
-
<div className="max-w-4xl mx-auto px-6 md:px-12 lg:px-16">
|
| 237 |
-
<div className="text-center mb-16">
|
| 238 |
-
<h2 className="text-[50px] md:text-[60px] font-headline font-semibold text-[var(--pure-white)] tracking-[-2.4px] mb-6 leading-tight drop-shadow-sm">
|
| 239 |
-
Pertanyaan Umum
|
| 240 |
-
</h2>
|
| 241 |
-
<p className="text-[var(--ube-300)] text-2xl max-w-2xl mx-auto">
|
| 242 |
-
Temukan jawaban cepat untuk pertanyaan seputar Pram.
|
| 243 |
-
</p>
|
| 244 |
-
</div>
|
| 245 |
-
|
| 246 |
-
<div className="bg-[var(--ube-900)]/40 backdrop-blur-sm border-2 border-[var(--ube-300)]/30 rounded-[32px] p-8 md:p-12 shadow-2xl">
|
| 247 |
-
<FaqItem
|
| 248 |
-
isDark
|
| 249 |
-
question="Apa itu Pram?"
|
| 250 |
-
answer="Pram adalah platform latihan ujian bahasa berbasis AI yang dirancang untuk membantu Anda berlatih dan menguasai bahasa asing melalui simulasi, bank soal interaktif, dan analitik performa."
|
| 251 |
-
/>
|
| 252 |
-
<FaqItem
|
| 253 |
-
isDark
|
| 254 |
-
question="Bagaimana cara kerja AI Generator?"
|
| 255 |
-
answer="Fitur AI Generator memungkinkan Anda membuat paket soal baru berdasarkan konteks atau topik tertentu. Anda cukup memasukkan teks acuan, dan AI Agent kami akan memproduksi soal secara otomatis."
|
| 256 |
-
/>
|
| 257 |
-
<FaqItem
|
| 258 |
-
isDark
|
| 259 |
-
question="Apakah Pram sepenuhnya gratis?"
|
| 260 |
-
answer="Platform Pram dapat digunakan secara gratis untuk fitur dasar. Untuk fitur generasi soal berbasis AI, kami menggunakan model Bring-Your-Own-Key (BYOK). Anda cukup memasukkan API Key OpenAI Anda."
|
| 261 |
-
/>
|
| 262 |
-
<FaqItem
|
| 263 |
-
isDark
|
| 264 |
-
question="Bahasa apa saja yang didukung oleh Pram?"
|
| 265 |
-
answer="Saat ini Pram mendukung latihan untuk berbagai ujian profisiensi bahasa populer seperti Bahasa Inggris (TOEFL, IELTS, TOEIC), Jepang (JLPT), Korea (TOPIK), dan banyak lagi."
|
| 266 |
-
/>
|
| 267 |
-
</div>
|
| 268 |
-
</div>
|
| 269 |
-
</section>
|
| 270 |
-
|
| 271 |
-
{/* SWATCH ROOM 2: Matcha CTA Section */}
|
| 272 |
-
<section className="w-full bg-[var(--matcha-800)] py-40 px-6 rounded-t-[40px] mt-[-40px] z-20 relative shadow-[0_-10px_40px_rgba(0,0,0,0.2)] border-t-2 border-[var(--matcha-600)]">
|
| 273 |
-
<div className="max-w-4xl mx-auto text-center space-y-12">
|
| 274 |
-
<h2 className="text-[60px] md:text-[80px] font-headline font-semibold text-[var(--pure-white)] tracking-[-3.2px] leading-[0.95] drop-shadow-md">
|
| 275 |
-
Siap Meningkatkan Skor Anda?
|
| 276 |
-
</h2>
|
| 277 |
-
<p className="text-2xl text-[var(--matcha-300)] max-w-2xl mx-auto">
|
| 278 |
-
Bergabung sekarang dan rasakan perbedaan belajar dengan teknologi yang berpusat pada perkembangan Anda.
|
| 279 |
-
</p>
|
| 280 |
-
<div className="pt-8">
|
| 281 |
-
<Link to="/login" className="inline-block">
|
| 282 |
-
<Button className="bg-[var(--pure-white)] text-[var(--clay-black)] hover:bg-[var(--oat-light)] rounded-[24px] h-[80px] px-14 text-2xl font-bold clay-shadow clay-hover">
|
| 283 |
-
Daftar Sekarang - Gratis
|
| 284 |
-
</Button>
|
| 285 |
-
</Link>
|
| 286 |
-
</div>
|
| 287 |
-
</div>
|
| 288 |
-
</section>
|
| 289 |
-
</main>
|
| 290 |
|
| 291 |
-
{/* Footer */}
|
| 292 |
-
<footer className="w-full bg-[var(--pure-white)] py-12 border-t-2 border-[var(--oat-border)] text-center relative z-30">
|
| 293 |
-
<p className="text-[var(--warm-charcoal)] font-semibold text-lg">
|
| 294 |
-
© {new Date().getFullYear()} Pram. Didesain dengan penuh kehangatan.
|
| 295 |
-
</p>
|
| 296 |
-
</footer>
|
| 297 |
</div>
|
| 298 |
);
|
| 299 |
}
|
|
|
|
| 2 |
import { Button } from "@pram/ui/components/button";
|
| 3 |
import { Card, CardContent } from "@pram/ui/components/card";
|
| 4 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
export function LandingPage() {
|
| 10 |
return (
|
| 11 |
<div className="min-h-screen bg-[var(--warm-cream)] flex flex-col font-sans selection:bg-[var(--matcha-300)] selection:text-[var(--clay-black)]">
|
|
|
|
| 12 |
{/* Navbar */}
|
| 13 |
<nav className="w-full px-6 py-4 md:px-12 lg:px-16 flex items-center justify-between max-w-7xl mx-auto z-50 sticky top-0 bg-[var(--warm-cream)] border-b-2 border-[var(--oat-border)]">
|
| 14 |
<div className="flex items-center gap-3">
|
|
|
|
| 16 |
<span className="font-headline font-semibold text-2xl tracking-[-0.64px] text-[var(--clay-black)] hidden sm:block">Pram</span>
|
| 17 |
</div>
|
| 18 |
<div className="flex items-center gap-2 sm:gap-4">
|
| 19 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
<Link to="/login">
|
| 21 |
<Button variant="ghost" className="text-[var(--clay-black)] font-semibold hover:bg-[var(--oat-light)] rounded-[12px] text-lg px-4 sm:px-6 h-12">
|
| 22 |
Masuk
|
|
|
|
| 30 |
</div>
|
| 31 |
</nav>
|
| 32 |
|
| 33 |
+
<main className="flex-1 flex flex-col items-center justify-center w-full relative">
|
| 34 |
{/* HUGE HERO SECTION */}
|
| 35 |
+
<section className="w-full px-6 md:px-12 lg:px-16 flex flex-col xl:flex-row items-center justify-between gap-12 lg:gap-16 relative max-w-[1440px] mx-auto">
|
| 36 |
<div className="flex-1 text-center xl:text-left space-y-8 z-10 w-full max-w-[800px] xl:max-w-none">
|
| 37 |
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--matcha-300)] border-2 border-[var(--matcha-800)] text-[var(--clay-black)] uppercase-label shadow-sm mx-auto xl:mx-0 transform -rotate-2">
|
| 38 |
<MaterialIcon name="auto_awesome" className="text-sm" />
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
);
|
| 86 |
}
|
apps/web/src/lib/community-links.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
export type CommunityPlatform = "whatsapp" | "discord";
|
| 2 |
-
|
| 3 |
-
export type CommunityChannel = {
|
| 4 |
-
id: string;
|
| 5 |
-
platform: CommunityPlatform;
|
| 6 |
-
name: string;
|
| 7 |
-
description: string;
|
| 8 |
-
/** null = belum live (tampil sebagai "Segera hadir") */
|
| 9 |
-
href: string | null;
|
| 10 |
-
icon: string;
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export const COMMUNITY_PLATFORM_META: Record<
|
| 14 |
-
CommunityPlatform,
|
| 15 |
-
{ label: string; icon: string; linkClass: string; iconClass: string }
|
| 16 |
-
> = {
|
| 17 |
-
whatsapp: {
|
| 18 |
-
label: "WhatsApp",
|
| 19 |
-
icon: "chat",
|
| 20 |
-
linkClass:
|
| 21 |
-
"bg-[#25D366]/10 border-[#25D366]/30 hover:bg-[#25D366]/20",
|
| 22 |
-
iconClass: "text-[#128C7E]",
|
| 23 |
-
},
|
| 24 |
-
discord: {
|
| 25 |
-
label: "Discord",
|
| 26 |
-
icon: "forum",
|
| 27 |
-
linkClass:
|
| 28 |
-
"bg-[#5865F2]/10 border-[#5865F2]/30 hover:bg-[#5865F2]/20",
|
| 29 |
-
iconClass: "text-[#5865F2]",
|
| 30 |
-
},
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
/** Satu sumber data — tambah Discord cukup isi `href` di entri bawah. */
|
| 34 |
-
export const COMMUNITY_CHANNELS: CommunityChannel[] = [
|
| 35 |
-
{
|
| 36 |
-
id: "pram-whatsapp",
|
| 37 |
-
platform: "whatsapp",
|
| 38 |
-
name: "Pram — Open Source",
|
| 39 |
-
description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Pram.",
|
| 40 |
-
href: "https://chat.whatsapp.com/JT6bDIXMQ5x6SUQCA1aDWB",
|
| 41 |
-
icon: "groups",
|
| 42 |
-
},
|
| 43 |
-
{
|
| 44 |
-
id: "learning-whatsapp",
|
| 45 |
-
platform: "whatsapp",
|
| 46 |
-
name: "pram Learning",
|
| 47 |
-
description: "Berbagi tips, trik, dan berita terbaru dari pram Learning.",
|
| 48 |
-
href: "https://chat.whatsapp.com/IrQOUGB3jhR7WnOoilCill",
|
| 49 |
-
icon: "school",
|
| 50 |
-
},
|
| 51 |
-
// Aktifkan saat server Discord siap — cukup isi href:
|
| 52 |
-
{
|
| 53 |
-
id: "pram-discord",
|
| 54 |
-
platform: "discord",
|
| 55 |
-
name: "Pram Discord",
|
| 56 |
-
description: "Diskusi async, bug report terstruktur, dan pengumuman fitur baru.",
|
| 57 |
-
href: null,
|
| 58 |
-
icon: "chat",
|
| 59 |
-
},
|
| 60 |
-
];
|
| 61 |
-
|
| 62 |
-
export const COMMUNITY_PROMPT_STORAGE_KEY = "pram_community_prompt_seen";
|
| 63 |
-
|
| 64 |
-
const PLATFORM_ORDER: CommunityPlatform[] = ["whatsapp", "discord"];
|
| 65 |
-
|
| 66 |
-
export function getCommunitySections() {
|
| 67 |
-
return PLATFORM_ORDER.map((platform) => ({
|
| 68 |
-
platform,
|
| 69 |
-
meta: COMMUNITY_PLATFORM_META[platform],
|
| 70 |
-
channels: COMMUNITY_CHANNELS.filter((c) => c.platform === platform),
|
| 71 |
-
})).filter((section) => section.channels.length > 0);
|
| 72 |
-
}
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apps/web/src/routes/__root.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
import type { QueryClient } from "@tanstack/react-query";
|
| 3 |
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
| 4 |
import { HeadContent, Outlet, createRootRouteWithContext, useRouterState } from "@tanstack/react-router";
|
|
@@ -62,10 +62,7 @@ export const Route = createRootRouteWithContext<RouterAppContext>()({
|
|
| 62 |
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" as const },
|
| 63 |
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" },
|
| 64 |
],
|
| 65 |
-
|
| 66 |
-
...(import.meta.env.PROD
|
| 67 |
-
? [{ src: "https://umami-analytic.pram-app.vercel.app/script.js", defer: true, "data-website-id": "67a18412-12c8-44ef-9cd3-e04238d37e9a" }]
|
| 68 |
-
: []),
|
| 69 |
{
|
| 70 |
type: "application/ld+json",
|
| 71 |
children: JSON.stringify({
|
|
|
|
| 1 |
+
import { Toaster } from "@pram/ui/components/sonner";
|
| 2 |
import type { QueryClient } from "@tanstack/react-query";
|
| 3 |
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
| 4 |
import { HeadContent, Outlet, createRootRouteWithContext, useRouterState } from "@tanstack/react-router";
|
|
|
|
| 62 |
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" as const },
|
| 63 |
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" },
|
| 64 |
],
|
| 65 |
+
|
|
|
|
|
|
|
|
|
|
| 66 |
{
|
| 67 |
type: "application/ld+json",
|
| 68 |
children: JSON.stringify({
|
apps/web/src/routes/dashboard.tsx
CHANGED
|
@@ -8,9 +8,8 @@ import { Button } from "@pram/ui/components/button";
|
|
| 8 |
import { Card, CardContent } from "@pram/ui/components/card";
|
| 9 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 10 |
import { TourGuide } from "@/components/TourGuide";
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
import { COMMUNITY_PROMPT_STORAGE_KEY } from "@/lib/community-links";
|
| 14 |
|
| 15 |
export const Route = createFileRoute("/dashboard")({
|
| 16 |
staticData: routeShell.app,
|
|
@@ -38,39 +37,8 @@ function formatDateShort(dateStr: string | Date | null) {
|
|
| 38 |
|
| 39 |
function HomeComponent() {
|
| 40 |
const { session } = Route.useRouteContext();
|
| 41 |
-
const [donationModalOpen, setDonationModalOpen] = useState(false);
|
| 42 |
-
const [donationTrigger, setDonationTrigger] = useState<"exam" | "generate" | null>(null);
|
| 43 |
-
const [communityModalOpen, setCommunityModalOpen] = useState(false);
|
| 44 |
|
| 45 |
-
const handleCommunityModalChange = (open: boolean) => {
|
| 46 |
-
setCommunityModalOpen(open);
|
| 47 |
-
if (!open) {
|
| 48 |
-
localStorage.setItem(COMMUNITY_PROMPT_STORAGE_KEY, "1");
|
| 49 |
-
}
|
| 50 |
-
};
|
| 51 |
|
| 52 |
-
useEffect(() => {
|
| 53 |
-
const prompt = localStorage.getItem("pendingDonationPrompt") as "exam" | "generate" | null;
|
| 54 |
-
const lastPromptStr = localStorage.getItem("lastDonationPromptTime");
|
| 55 |
-
const lastPromptTime = lastPromptStr ? parseInt(lastPromptStr, 10) : 0;
|
| 56 |
-
const COOLDOWN = 24 * 60 * 60 * 1000;
|
| 57 |
-
const now = Date.now();
|
| 58 |
-
|
| 59 |
-
let showedDonation = false;
|
| 60 |
-
if (prompt) {
|
| 61 |
-
localStorage.removeItem("pendingDonationPrompt");
|
| 62 |
-
if (now - lastPromptTime > COOLDOWN) {
|
| 63 |
-
setDonationTrigger(prompt);
|
| 64 |
-
setDonationModalOpen(true);
|
| 65 |
-
localStorage.setItem("lastDonationPromptTime", now.toString());
|
| 66 |
-
showedDonation = true;
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (!showedDonation && !localStorage.getItem(COMMUNITY_PROMPT_STORAGE_KEY)) {
|
| 71 |
-
setCommunityModalOpen(true);
|
| 72 |
-
}
|
| 73 |
-
}, []);
|
| 74 |
|
| 75 |
const overview = useQuery(trpc.stats.overview.queryOptions());
|
| 76 |
const recentAttempts = useQuery(
|
|
@@ -90,12 +58,8 @@ function HomeComponent() {
|
|
| 90 |
|
| 91 |
return (
|
| 92 |
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-6xl mx-auto bg-[var(--warm-cream)]">
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
onOpenChange={setDonationModalOpen}
|
| 96 |
-
triggerAction={donationTrigger}
|
| 97 |
-
/>
|
| 98 |
-
<CommunityModal isOpen={communityModalOpen} onOpenChange={handleCommunityModalChange} />
|
| 99 |
{/* Header */}
|
| 100 |
<section className="mb-8">
|
| 101 |
<h1 className="text-4xl font-headline font-extrabold text-[var(--clay-black)] tracking-tight">
|
|
|
|
| 8 |
import { Card, CardContent } from "@pram/ui/components/card";
|
| 9 |
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
| 10 |
import { TourGuide } from "@/components/TourGuide";
|
| 11 |
+
|
| 12 |
+
|
|
|
|
| 13 |
|
| 14 |
export const Route = createFileRoute("/dashboard")({
|
| 15 |
staticData: routeShell.app,
|
|
|
|
| 37 |
|
| 38 |
function HomeComponent() {
|
| 39 |
const { session } = Route.useRouteContext();
|
|
|
|
|
|
|
|
|
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
const overview = useQuery(trpc.stats.overview.queryOptions());
|
| 44 |
const recentAttempts = useQuery(
|
|
|
|
| 58 |
|
| 59 |
return (
|
| 60 |
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-6xl mx-auto bg-[var(--warm-cream)]">
|
| 61 |
+
|
| 62 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
{/* Header */}
|
| 64 |
<section className="mb-8">
|
| 65 |
<h1 className="text-4xl font-headline font-extrabold text-[var(--clay-black)] tracking-tight">
|
apps/web/vite.config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
| 3 |
import react from "@vitejs/plugin-react";
|
| 4 |
import { defineConfig } from "vite";
|
|
@@ -24,16 +24,7 @@ export default defineConfig({
|
|
| 24 |
},
|
| 25 |
},
|
| 26 |
plugins: [
|
| 27 |
-
|
| 28 |
-
name: "inject-umami",
|
| 29 |
-
transformIndexHtml(html) {
|
| 30 |
-
return html.replace(
|
| 31 |
-
"</head>",
|
| 32 |
-
'<script defer src="https://umami-analytic.pram-app.vercel.app/script.js" data-website-id="67a18412-12c8-44ef-9cd3-e04238d37e9a"></script></head>',
|
| 33 |
-
);
|
| 34 |
-
},
|
| 35 |
-
apply: "build",
|
| 36 |
-
},
|
| 37 |
tailwindcss(),
|
| 38 |
tanstackRouter({
|
| 39 |
target: "react",
|
|
|
|
| 1 |
+
import tailwindcss from "@tailwindcss/vite";
|
| 2 |
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
| 3 |
import react from "@vitejs/plugin-react";
|
| 4 |
import { defineConfig } from "vite";
|
|
|
|
| 24 |
},
|
| 25 |
},
|
| 26 |
plugins: [
|
| 27 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
tailwindcss(),
|
| 29 |
tanstackRouter({
|
| 30 |
target: "react",
|