Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4c86d473c | |||
| ebbad9bc9e | |||
| da408dcb5d | |||
| d2edafdf77 | |||
| eed168b1e5 | |||
| c5f7f250bc | |||
| 5397a601c6 | |||
| 35cc0f9a47 | |||
| d80111a9b7 | |||
| b3cf462228 | |||
| 21dbe336ca |
@ -4,6 +4,12 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700&family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&family=Sorts+Mill+Goudy:ital@0;1&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<script src="https://www.geogebra.org/apps/deployggb.js"></script>
|
<script src="https://www.geogebra.org/apps/deployggb.js"></script>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
|
|||||||
22
src/App.tsx
22
src/App.tsx
@ -1,5 +1,4 @@
|
|||||||
import "katex/dist/katex.min.css";
|
import { Suspense, lazy } from "react";
|
||||||
|
|
||||||
import { Home } from "./pages/student/Home";
|
import { Home } from "./pages/student/Home";
|
||||||
import {
|
import {
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
@ -19,10 +18,13 @@ import { StudentLayout } from "./pages/student/StudentLayout";
|
|||||||
import { TargetedPractice } from "./pages/student/targeted-practice/page";
|
import { TargetedPractice } from "./pages/student/targeted-practice/page";
|
||||||
import { Drills } from "./pages/student/drills/page";
|
import { Drills } from "./pages/student/drills/page";
|
||||||
import { HardTestModules } from "./pages/student/hard-test-modules/page";
|
import { HardTestModules } from "./pages/student/hard-test-modules/page";
|
||||||
import { QuestMap } from "./pages/student/QuestMap";
|
|
||||||
import { Register } from "./pages/auth/Register";
|
import { Register } from "./pages/auth/Register";
|
||||||
import { PracticeSheetList } from "./pages/student/practice-sheet/page";
|
import { PracticeSheetList } from "./pages/student/practice-sheet/page";
|
||||||
|
|
||||||
|
const QuestMap = lazy(() =>
|
||||||
|
import("./pages/student/QuestMap").then((m) => ({ default: m.QuestMap })),
|
||||||
|
);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -63,11 +65,11 @@ function App() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "quests",
|
path: "quests",
|
||||||
element: <QuestMap />,
|
element: (
|
||||||
},
|
<Suspense fallback={<div style={{ minHeight: "100vh" }} />}>
|
||||||
{
|
<QuestMap />
|
||||||
path: "practice/:sheetId",
|
</Suspense>
|
||||||
element: <Pretest />,
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "practice/targeted-practice",
|
path: "practice/targeted-practice",
|
||||||
@ -85,6 +87,10 @@ function App() {
|
|||||||
path: "practice/practice-sheet",
|
path: "practice/practice-sheet",
|
||||||
element: <PracticeSheetList />,
|
element: <PracticeSheetList />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "practice/:sheetId",
|
||||||
|
element: <Pretest />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import type { QuestNode, ClaimedRewardResponse } from "../types/quest";
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const S = `
|
const S = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=Nunito:wght@800;900&display=swap');
|
|
||||||
|
|
||||||
/* ══ FULL SCREEN OVERLAY ══ */
|
/* ══ FULL SCREEN OVERLAY ══ */
|
||||||
.com-overlay {
|
.com-overlay {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@600;700&display=swap');
|
|
||||||
|
|
||||||
.cc-btn {
|
.cc-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -12,7 +12,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600&display=swap');
|
|
||||||
|
|
||||||
.clp-wrap {
|
.clp-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
83
src/components/ErrorBoundary.tsx
Normal file
83
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Component } from "react";
|
||||||
|
import type { ErrorInfo, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): State {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
console.error("Uncaught error:", error, info.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: "'Nunito', sans-serif",
|
||||||
|
background: "#fffbf4",
|
||||||
|
padding: "2rem",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 900,
|
||||||
|
color: "#1e1b4b",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Something went wrong
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
color: "#6b7280",
|
||||||
|
marginBottom: "1.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
An unexpected error occurred. Please try refreshing the page.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
style={{
|
||||||
|
padding: "0.7rem 1.4rem",
|
||||||
|
borderRadius: "100px",
|
||||||
|
border: "none",
|
||||||
|
background: "linear-gradient(135deg, #7c3aed, #a855f7)",
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "'Nunito', sans-serif",
|
||||||
|
fontSize: "0.88rem",
|
||||||
|
fontWeight: 800,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reload page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|||||||
import { Drawer, DrawerContent, DrawerTrigger } from "./ui/drawer";
|
import { Drawer, DrawerContent, DrawerTrigger } from "./ui/drawer";
|
||||||
import { PredictedScoreCard } from "./PredictedScoreCard";
|
import { PredictedScoreCard } from "./PredictedScoreCard";
|
||||||
import { ChestOpenModal } from "./ChestOpenModal";
|
import { ChestOpenModal } from "./ChestOpenModal";
|
||||||
import { generateArcTheme } from "../pages/student/QuestMap";
|
import { generateArcTheme } from "../utils/arcTheme";
|
||||||
import { InventoryButton } from "./InventoryButton";
|
import { InventoryButton } from "./InventoryButton";
|
||||||
|
|
||||||
// ─── Requirement helpers ──────────────────────────────────────────────────────
|
// ─── Requirement helpers ──────────────────────────────────────────────────────
|
||||||
@ -43,7 +43,6 @@ const REQ_LABEL: Record<string, string> = {
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&family=Cinzel:wght@700;900&display=swap');
|
|
||||||
|
|
||||||
/* ════ SHARED ANIMATION ════ */
|
/* ════ SHARED ANIMATION ════ */
|
||||||
@keyframes hcIn {
|
@keyframes hcIn {
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { InventoryModal } from "./InventoryModal";
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const BTN_STYLES = `
|
const BTN_STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@800;900&family=Cinzel:wght@700&display=swap');
|
|
||||||
|
|
||||||
/* ── Inventory trigger button ── */
|
/* ── Inventory trigger button ── */
|
||||||
.inv-btn {
|
.inv-btn {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { api } from "../utils/api";
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700;900&family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
/* ══ OVERLAY ══ */
|
/* ══ OVERLAY ══ */
|
||||||
.inv-overlay {
|
.inv-overlay {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ const UUID_REGEX =
|
|||||||
const isVideoLesson = (id: string) => UUID_REGEX.test(id);
|
const isVideoLesson = (id: string) => UUID_REGEX.test(id);
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
.lm-content {
|
.lm-content {
|
||||||
font-family: 'Nunito', sans-serif;
|
font-family: 'Nunito', sans-serif;
|
||||||
@ -156,6 +155,7 @@ export const LessonModal = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: LessonModalProps) => {
|
}: LessonModalProps) => {
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [lesson, setLesson] = useState<LessonDetails | null>(null);
|
const [lesson, setLesson] = useState<LessonDetails | null>(null);
|
||||||
@ -196,12 +196,6 @@ export const LessonModal = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) throw new Error("No auth storage");
|
|
||||||
const {
|
|
||||||
// @ts-ignore
|
|
||||||
state: { token },
|
|
||||||
} = JSON.parse(authStorage) as { state?: { token?: string } };
|
|
||||||
if (!token) throw new Error("No token");
|
if (!token) throw new Error("No token");
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@ -83,7 +83,6 @@ const useCountUp = (target: number, duration = 900) => {
|
|||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
.psc-card {
|
.psc-card {
|
||||||
background: white;
|
background: white;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { X, Lock } from "lucide-react";
|
import { X, Lock } from "lucide-react";
|
||||||
import type { QuestNode, QuestArc } from "../types/quest";
|
import type { QuestNode, QuestArc } from "../types/quest";
|
||||||
// Re-use the same theme generator as QuestMap so island colours are consistent
|
// Re-use the same theme generator as QuestMap so island colours are consistent
|
||||||
import { generateArcTheme } from "../pages/student/QuestMap";
|
import { generateArcTheme } from "../utils/arcTheme";
|
||||||
|
|
||||||
// ─── Requirement helpers (mirrors QuestMap / InfoHeader) ──────────────────────
|
// ─── Requirement helpers (mirrors QuestMap / InfoHeader) ──────────────────────
|
||||||
const REQ_LABEL: Record<string, string> = {
|
const REQ_LABEL: Record<string, string> = {
|
||||||
@ -28,7 +28,6 @@ const reqIcon = (type: string): string =>
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@600;700;900&family=Sorts+Mill+Goudy:ital@0;1&family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
/* ══ OVERLAY ══ */
|
/* ══ OVERLAY ══ */
|
||||||
.qnm-overlay {
|
.qnm-overlay {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import "katex/dist/katex.min.css";
|
||||||
import { Component, type ReactNode } from "react";
|
import { Component, type ReactNode } from "react";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { BlockMath, InlineMath } from "react-katex";
|
import { BlockMath, InlineMath } from "react-katex";
|
||||||
|
|||||||
@ -188,7 +188,6 @@ const highlightText = (text: string, query: string) => {
|
|||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
.so-overlay {
|
.so-overlay {
|
||||||
position: fixed; inset: 0; z-index: 50;
|
position: fixed; inset: 0; z-index: 50;
|
||||||
|
|||||||
@ -2,9 +2,12 @@ import { StrictMode } from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
|
import { ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<ErrorBoundary>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ interface LocationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import {
|
|||||||
import { api } from "../../utils/api";
|
import { api } from "../../utils/api";
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -312,6 +311,7 @@ const PAGE_SIZE = 6;
|
|||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [practiceSheets, setPracticeSheets] = useState<PracticeSheet[]>([]);
|
const [practiceSheets, setPracticeSheets] = useState<PracticeSheet[]>([]);
|
||||||
@ -337,14 +337,8 @@ export const Home = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
try {
|
try {
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
const {
|
|
||||||
state: { token },
|
|
||||||
} = JSON.parse(authStorage);
|
|
||||||
if (!token) return;
|
|
||||||
const sheets = await api.getPracticeSheets(token, 1, 10);
|
const sheets = await api.getPracticeSheets(token, 1, 10);
|
||||||
setPracticeSheets(sheets.data);
|
setPracticeSheets(sheets.data);
|
||||||
sort(sheets.data);
|
sort(sheets.data);
|
||||||
|
|||||||
@ -31,7 +31,6 @@ const DOTS = [
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -465,6 +464,7 @@ const VideoCard = ({ lesson, index, searchQuery, onClick }: VideoCardProps) => (
|
|||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
export const Lessons = () => {
|
export const Lessons = () => {
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
|
const token = useAuthStore((s) => s.token);
|
||||||
|
|
||||||
// Video lessons from API — typed as Lesson[]
|
// Video lessons from API — typed as Lesson[]
|
||||||
const [allVideos, setAllVideos] = useState<Lesson[]>([]);
|
const [allVideos, setAllVideos] = useState<Lesson[]>([]);
|
||||||
@ -486,17 +486,9 @@ export const Lessons = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
try {
|
try {
|
||||||
setLessonLoading(true);
|
setLessonLoading(true);
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
|
|
||||||
const {
|
|
||||||
// @ts-ignore
|
|
||||||
state: { token },
|
|
||||||
} = JSON.parse(authStorage) as { state?: { token?: string } };
|
|
||||||
if (!token) return;
|
|
||||||
const response = await api.fetchLessonVideos(token);
|
const response = await api.fetchLessonVideos(token);
|
||||||
// response matches LessonsResponse: { data: Lesson[], pagination: ... }
|
// response matches LessonsResponse: { data: Lesson[], pagination: ... }
|
||||||
setAllVideos(response.data);
|
setAllVideos(response.data);
|
||||||
|
|||||||
@ -12,7 +12,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
import { useQuestStore } from "../../stores/useQuestStore";
|
import { useQuestStore } from "../../stores/useQuestStore";
|
||||||
import { useAuthStore } from "../../stores/authStore";
|
import { useAuthStore } from "../../stores/authStore";
|
||||||
import { api } from "../../utils/api";
|
import { api } from "../../utils/api";
|
||||||
|
import { generateArcTheme, mkRng, strToSeed, type ArcTheme } from "../../utils/arcTheme";
|
||||||
import { QuestNodeModal } from "../../components/QuestNodeModal";
|
import { QuestNodeModal } from "../../components/QuestNodeModal";
|
||||||
import { ChestOpenModal } from "../../components/ChestOpenModal";
|
import { ChestOpenModal } from "../../components/ChestOpenModal";
|
||||||
import { InfoHeader } from "../../components/InfoHeader";
|
import { InfoHeader } from "../../components/InfoHeader";
|
||||||
@ -21,22 +22,7 @@ const TOP_PAD = 80;
|
|||||||
const ROW_H = 520; // vertical step per island (increased for more separation)
|
const ROW_H = 520; // vertical step per island (increased for more separation)
|
||||||
|
|
||||||
// ─── Seeded RNG ───────────────────────────────────────────────────────────────
|
// ─── Seeded RNG ───────────────────────────────────────────────────────────────
|
||||||
const mkRng = (seed: number) => {
|
// mkRng, strToSeed imported from ../../utils/arcTheme
|
||||||
let s = seed >>> 0;
|
|
||||||
return () => {
|
|
||||||
s += 0x6d2b79f5;
|
|
||||||
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
||||||
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
||||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const strToSeed = (str: string) => {
|
|
||||||
let h = 5381;
|
|
||||||
for (let i = 0; i < str.length; i++)
|
|
||||||
h = (Math.imul(h, 33) ^ str.charCodeAt(i)) >>> 0;
|
|
||||||
return h;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Random island positions ──────────────────────────────────────────────────
|
// ─── Random island positions ──────────────────────────────────────────────────
|
||||||
// Generates organic-feeling positions that zigzag downward with random offsets.
|
// Generates organic-feeling positions that zigzag downward with random offsets.
|
||||||
@ -102,8 +88,7 @@ const generateIslandPositions = (
|
|||||||
|
|
||||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Nunito+Sans:wght@400;600;700&family=Cinzel:wght@700;900&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Sorts+Mill+Goudy:ital@0;1&display=swap');
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
@ -601,92 +586,7 @@ const ERROR_STYLES = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// ─── Arc theme ────────────────────────────────────────────────────────────────
|
// ─── Arc theme ────────────────────────────────────────────────────────────────
|
||||||
export interface ArcTheme {
|
// ArcTheme, generateArcTheme imported from ../../utils/arcTheme
|
||||||
accent: string;
|
|
||||||
accentDark: string;
|
|
||||||
bgFrom: string;
|
|
||||||
bgTo: string;
|
|
||||||
emoji: string;
|
|
||||||
terrain: { l: string; m: string; d: string; s: string };
|
|
||||||
decos: [string, string, string];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DECO_SETS: [string, string, string][] = [
|
|
||||||
["🌴", "🌿", "🌴"],
|
|
||||||
["🌵", "🏺", "🌵"],
|
|
||||||
["☁️", "✨", "☁️"],
|
|
||||||
["🪨", "🌾", "🪨"],
|
|
||||||
["🍄", "🌸", "🍄"],
|
|
||||||
["🔥", "💀", "🔥"],
|
|
||||||
["❄️", "🌨️", "❄️"],
|
|
||||||
["🌺", "🦜", "🌺"],
|
|
||||||
];
|
|
||||||
|
|
||||||
const hslToHex = (h: number, s: number, l: number) => {
|
|
||||||
const a = s * Math.min(l, 1 - l);
|
|
||||||
const f = (n: number) => {
|
|
||||||
const k = (n + h * 12) % 12;
|
|
||||||
const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
||||||
return Math.round(255 * c)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
};
|
|
||||||
return `#${f(0)}${f(8)}${f(4)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateArcTheme = (arc: QuestArc): ArcTheme => {
|
|
||||||
const rng = mkRng(strToSeed(arc.id));
|
|
||||||
const anchors = [150, 165, 180, 200, 230, 260];
|
|
||||||
const baseHue =
|
|
||||||
anchors[Math.floor(rng() * anchors.length)] + (rng() - 0.5) * 8;
|
|
||||||
const satBase = 0.48 + rng() * 0.18;
|
|
||||||
const satTerrain = Math.min(0.8, satBase + 0.12);
|
|
||||||
const accentLightL = 0.48 + rng() * 0.12;
|
|
||||||
const accentDarkL = 0.22 + rng() * 0.06;
|
|
||||||
const bgFromL = 0.04 + rng() * 0.06;
|
|
||||||
const bgToL = 0.1 + rng() * 0.06;
|
|
||||||
const accent = hslToHex(baseHue, satBase, accentLightL);
|
|
||||||
const accentDark = hslToHex(
|
|
||||||
baseHue + (rng() * 6 - 3),
|
|
||||||
Math.max(0.35, satBase - 0.08),
|
|
||||||
accentDarkL,
|
|
||||||
);
|
|
||||||
const bgFrom = hslToHex(
|
|
||||||
baseHue + (rng() * 10 - 5),
|
|
||||||
0.1 + rng() * 0.06,
|
|
||||||
bgFromL,
|
|
||||||
);
|
|
||||||
const bgTo = hslToHex(baseHue + (6 + rng() * 12), 0.08 + rng() * 0.06, bgToL);
|
|
||||||
const tL = hslToHex(
|
|
||||||
baseHue + 10 + rng() * 6,
|
|
||||||
Math.min(0.85, satTerrain),
|
|
||||||
0.36 + rng() * 0.08,
|
|
||||||
);
|
|
||||||
const tM = hslToHex(
|
|
||||||
baseHue + (rng() * 6 - 3),
|
|
||||||
Math.min(0.72, satTerrain - 0.06),
|
|
||||||
0.24 + rng() * 0.06,
|
|
||||||
);
|
|
||||||
const tD = hslToHex(
|
|
||||||
baseHue + (rng() * 8 - 4),
|
|
||||||
Math.max(0.38, satBase - 0.18),
|
|
||||||
0.1 + rng() * 0.04,
|
|
||||||
);
|
|
||||||
const sd = parseInt(tD.slice(1, 3), 16);
|
|
||||||
const sg = parseInt(tD.slice(3, 5), 16);
|
|
||||||
const sb = parseInt(tD.slice(5, 7), 16);
|
|
||||||
const emojis = ["🌿", "🌲", "🌳", "🌺", "🪨", "🍄", "🌵"];
|
|
||||||
const emoji = emojis[Math.floor(rng() * emojis.length)];
|
|
||||||
return {
|
|
||||||
accent,
|
|
||||||
accentDark,
|
|
||||||
bgFrom,
|
|
||||||
bgTo,
|
|
||||||
emoji,
|
|
||||||
terrain: { l: tL, m: tM, d: tD, s: `rgba(${sd},${sg},${sb},0.6)` },
|
|
||||||
decos: DECO_SETS[Math.floor(rng() * DECO_SETS.length)],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const themeCache = new Map<string, ArcTheme>();
|
const themeCache = new Map<string, ArcTheme>();
|
||||||
const getArcTheme = (arc: QuestArc): ArcTheme => {
|
const getArcTheme = (arc: QuestArc): ArcTheme => {
|
||||||
|
|||||||
@ -31,7 +31,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -415,6 +414,7 @@ const EmptyState = () => (
|
|||||||
|
|
||||||
export const Rewards = () => {
|
export const Rewards = () => {
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
const [time, setTime] = useState("today");
|
const [time, setTime] = useState("today");
|
||||||
const [activeTab, setActiveTab] = useState<TabId>("xp");
|
const [activeTab, setActiveTab] = useState<TabId>("xp");
|
||||||
const [leaderboard, setLeaderboard] = useState<Leaderboard | undefined>();
|
const [leaderboard, setLeaderboard] = useState<Leaderboard | undefined>();
|
||||||
@ -431,20 +431,15 @@ export const Rewards = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
const parsed = JSON.parse(authStorage) as {
|
|
||||||
state?: { token?: string };
|
|
||||||
} | null;
|
|
||||||
const token = parsed?.state?.token;
|
|
||||||
if (!token) return;
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
const timeframe =
|
||||||
|
activeTab === "streaks" ? "all_time" : (TIME_MAP[time] ?? "daily");
|
||||||
const response = await api.fetchLeaderboard(
|
const response = await api.fetchLeaderboard(
|
||||||
token,
|
token,
|
||||||
activeTab,
|
activeTab,
|
||||||
TIME_MAP[time] ?? "daily",
|
timeframe,
|
||||||
);
|
);
|
||||||
setLeaderboard(response);
|
setLeaderboard(response);
|
||||||
// ✅ FIX 1: Guard against null user_rank before accessing its properties
|
// ✅ FIX 1: Guard against null user_rank before accessing its properties
|
||||||
@ -563,9 +558,13 @@ export const Rewards = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild disabled={activeTab === "streaks"}>
|
||||||
<button className="rw-filter-btn">
|
<button
|
||||||
{formatTimeLabel(time)} <ChevronDown size={13} />
|
className="rw-filter-btn"
|
||||||
|
style={activeTab === "streaks" ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
|
||||||
|
>
|
||||||
|
{activeTab === "streaks" ? "All Time" : formatTimeLabel(time)}{" "}
|
||||||
|
<ChevronDown size={13} />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
|
|||||||
@ -57,8 +57,7 @@ const QUEST_NAV_ITEMS = NAV_ITEMS.map((item) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700;800;900&family=Cinzel:wght@700&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Sorts+Mill+Goudy:ital@0;1&display=swap');
|
|
||||||
|
|
||||||
/* ══ DEFAULT dock (cream frosted glass) ══ */
|
/* ══ DEFAULT dock (cream frosted glass) ══ */
|
||||||
.sl-dock-wrap {
|
.sl-dock-wrap {
|
||||||
|
|||||||
@ -22,7 +22,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -281,6 +280,7 @@ const STYLES = `
|
|||||||
|
|
||||||
export const Drills = () => {
|
export const Drills = () => {
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [direction, setDirection] = useState<1 | -1>(1);
|
const [direction, setDirection] = useState<1 | -1>(1);
|
||||||
@ -319,16 +319,9 @@ export const Drills = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAllTopics = async () => {
|
const fetchAllTopics = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
const parsed = JSON.parse(authStorage) as {
|
|
||||||
state?: { token?: string };
|
|
||||||
} | null;
|
|
||||||
const token = parsed?.state?.token;
|
|
||||||
if (!token) return;
|
|
||||||
const response = await api.fetchAllTopics(token);
|
const response = await api.fetchAllTopics(token);
|
||||||
setTopics(response);
|
setTopics(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { api } from "../../../utils/api";
|
import { api } from "../../../utils/api";
|
||||||
import { type PracticeSheet } from "../../../types/sheet";
|
import { type PracticeSheet } from "../../../types/sheet";
|
||||||
|
import { useAuthStore } from "../../../stores/authStore";
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────
|
/* ─────────────────────────────────────────────
|
||||||
Ambient decoration
|
Ambient decoration
|
||||||
@ -415,7 +416,6 @@ const EmptyState = ({ query }: { query: string }) => (
|
|||||||
Styles
|
Styles
|
||||||
───────────────────────────────────────────── */
|
───────────────────────────────────────────── */
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -700,6 +700,7 @@ type ViewMode = "standard" | "compact";
|
|||||||
|
|
||||||
export const PracticeSheetList = () => {
|
export const PracticeSheetList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
const [sheets, setSheets] = useState<PracticeSheet[]>([]);
|
const [sheets, setSheets] = useState<PracticeSheet[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -707,13 +708,8 @@ export const PracticeSheetList = () => {
|
|||||||
const [viewMode, setViewMode] = useState<ViewMode>("compact");
|
const [viewMode, setViewMode] = useState<ViewMode>("compact");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
const {
|
|
||||||
state: { token },
|
|
||||||
} = JSON.parse(authStorage);
|
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
setLoading(true);
|
||||||
api
|
api
|
||||||
.getPracticeSheets(token, 1, 10)
|
.getPracticeSheets(token, 1, 10)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|||||||
@ -23,7 +23,6 @@ const DOTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -227,6 +226,7 @@ export const Pretest = () => {
|
|||||||
const { setSheetId, setMode, storeDuration, setQuestionCount } =
|
const { setSheetId, setMode, storeDuration, setQuestionCount } =
|
||||||
useExamConfigStore();
|
useExamConfigStore();
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const token = useAuthStore((state) => state.token);
|
||||||
const { sheetId } = useParams<{ sheetId: string }>();
|
const { sheetId } = useParams<{ sheetId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -246,15 +246,9 @@ export const Pretest = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
async function fetchSheet(id: string) {
|
async function fetchSheet(id: string) {
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
const data = await api.getPracticeSheetById(token!, id);
|
||||||
if (!authStorage) return;
|
|
||||||
const {
|
|
||||||
state: { token },
|
|
||||||
} = JSON.parse(authStorage);
|
|
||||||
if (!token) return;
|
|
||||||
const data = await api.getPracticeSheetById(token, id);
|
|
||||||
setPracticeSheet(data);
|
setPracticeSheet(data);
|
||||||
}
|
}
|
||||||
fetchSheet(sheetId!);
|
fetchSheet(sheetId!);
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { useAuthStore } from "../../../stores/authStore";
|
|||||||
|
|
||||||
// ─── Shared styles injected once ─────────────────────────────────────────────
|
// ─── Shared styles injected once ─────────────────────────────────────────────
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,6 @@ const DOTS = [
|
|||||||
|
|
||||||
// ─── Global Styles ────────────────────────────────────────────────────────────
|
// ─── Global Styles ────────────────────────────────────────────────────────────
|
||||||
const GLOBAL_STYLES = `
|
const GLOBAL_STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,6 @@ const getSectionMeta = (section?: string) =>
|
|||||||
SECTION_META[section ?? ""] ?? SECTION_META["default"];
|
SECTION_META[section ?? ""] ?? SECTION_META["default"];
|
||||||
|
|
||||||
const STYLES = `
|
const STYLES = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Nunito+Sans:wght@400;600;700&display=swap');
|
|
||||||
|
|
||||||
:root { --content-max: 1100px; }
|
:root { --content-max: 1100px; }
|
||||||
|
|
||||||
@ -508,16 +507,9 @@ export const TargetedPractice = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAllTopics = async () => {
|
const fetchAllTopics = async () => {
|
||||||
if (!user) return;
|
if (!user || !token) return;
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const authStorage = localStorage.getItem("auth-storage");
|
|
||||||
if (!authStorage) return;
|
|
||||||
const parsed = JSON.parse(authStorage) as {
|
|
||||||
state?: { token?: string };
|
|
||||||
} | null;
|
|
||||||
const token = parsed?.state?.token;
|
|
||||||
if (!token) return;
|
|
||||||
const response = await api.fetchAllTopics(token);
|
const response = await api.fetchAllTopics(token);
|
||||||
setTopics(response);
|
setTopics(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
|
|
||||||
set({
|
set({
|
||||||
registrationMessage: response.message,
|
registrationMessage: response.message,
|
||||||
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -119,3 +120,8 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Safe non-hook token accessor for use outside React components or in callbacks */
|
||||||
|
export function getAuthToken(): string | null {
|
||||||
|
return useAuthStore.getState().token;
|
||||||
|
}
|
||||||
|
|||||||
105
src/utils/arcTheme.ts
Normal file
105
src/utils/arcTheme.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import type { QuestArc } from "../types/quest";
|
||||||
|
|
||||||
|
export interface ArcTheme {
|
||||||
|
accent: string;
|
||||||
|
accentDark: string;
|
||||||
|
bgFrom: string;
|
||||||
|
bgTo: string;
|
||||||
|
emoji: string;
|
||||||
|
terrain: { l: string; m: string; d: string; s: string };
|
||||||
|
decos: [string, string, string];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mkRng = (seed: number) => {
|
||||||
|
let s = seed >>> 0;
|
||||||
|
return () => {
|
||||||
|
s += 0x6d2b79f5;
|
||||||
|
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const strToSeed = (str: string) => {
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < str.length; i++)
|
||||||
|
h = (Math.imul(h, 33) ^ str.charCodeAt(i)) >>> 0;
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hslToHex = (h: number, s: number, l: number) => {
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h * 12) % 12;
|
||||||
|
const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||||
|
return Math.round(255 * c)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0");
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DECO_SETS: [string, string, string][] = [
|
||||||
|
["\u{1F334}", "\u{1F33F}", "\u{1F334}"],
|
||||||
|
["\u{1F335}", "\u{1F3FA}", "\u{1F335}"],
|
||||||
|
["\u2601\uFE0F", "\u2728", "\u2601\uFE0F"],
|
||||||
|
["\u{1FAA8}", "\u{1F33E}", "\u{1FAA8}"],
|
||||||
|
["\u{1F344}", "\u{1F338}", "\u{1F344}"],
|
||||||
|
["\u{1F525}", "\u{1F480}", "\u{1F525}"],
|
||||||
|
["\u2744\uFE0F", "\u{1F328}\uFE0F", "\u2744\uFE0F"],
|
||||||
|
["\u{1F33A}", "\u{1F99C}", "\u{1F33A}"],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const generateArcTheme = (arc: QuestArc): ArcTheme => {
|
||||||
|
const rng = mkRng(strToSeed(arc.id));
|
||||||
|
const anchors = [150, 165, 180, 200, 230, 260];
|
||||||
|
const baseHue =
|
||||||
|
anchors[Math.floor(rng() * anchors.length)] + (rng() - 0.5) * 8;
|
||||||
|
const satBase = 0.48 + rng() * 0.18;
|
||||||
|
const satTerrain = Math.min(0.8, satBase + 0.12);
|
||||||
|
const accentLightL = 0.48 + rng() * 0.12;
|
||||||
|
const accentDarkL = 0.22 + rng() * 0.06;
|
||||||
|
const bgFromL = 0.04 + rng() * 0.06;
|
||||||
|
const bgToL = 0.1 + rng() * 0.06;
|
||||||
|
const accent = hslToHex(baseHue, satBase, accentLightL);
|
||||||
|
const accentDark = hslToHex(
|
||||||
|
baseHue + (rng() * 6 - 3),
|
||||||
|
Math.max(0.35, satBase - 0.08),
|
||||||
|
accentDarkL,
|
||||||
|
);
|
||||||
|
const bgFrom = hslToHex(
|
||||||
|
baseHue + (rng() * 10 - 5),
|
||||||
|
0.1 + rng() * 0.06,
|
||||||
|
bgFromL,
|
||||||
|
);
|
||||||
|
const bgTo = hslToHex(baseHue + (6 + rng() * 12), 0.08 + rng() * 0.06, bgToL);
|
||||||
|
const tL = hslToHex(
|
||||||
|
baseHue + 10 + rng() * 6,
|
||||||
|
Math.min(0.85, satTerrain),
|
||||||
|
0.36 + rng() * 0.08,
|
||||||
|
);
|
||||||
|
const tM = hslToHex(
|
||||||
|
baseHue + (rng() * 6 - 3),
|
||||||
|
Math.min(0.72, satTerrain - 0.06),
|
||||||
|
0.24 + rng() * 0.06,
|
||||||
|
);
|
||||||
|
const tD = hslToHex(
|
||||||
|
baseHue + (rng() * 8 - 4),
|
||||||
|
Math.max(0.38, satBase - 0.18),
|
||||||
|
0.1 + rng() * 0.04,
|
||||||
|
);
|
||||||
|
const sd = parseInt(tD.slice(1, 3), 16);
|
||||||
|
const sg = parseInt(tD.slice(3, 5), 16);
|
||||||
|
const sb = parseInt(tD.slice(5, 7), 16);
|
||||||
|
const emojis = ["\u{1F33F}", "\u{1F332}", "\u{1F333}", "\u{1F33A}", "\u{1FAA8}", "\u{1F344}", "\u{1F335}"];
|
||||||
|
const emoji = emojis[Math.floor(rng() * emojis.length)];
|
||||||
|
return {
|
||||||
|
accent,
|
||||||
|
accentDark,
|
||||||
|
bgFrom,
|
||||||
|
bgTo,
|
||||||
|
emoji,
|
||||||
|
terrain: { l: tL, m: tM, d: tD, s: `rgba(${sd},${sg},${sb},0.6)` },
|
||||||
|
decos: DECO_SETS[Math.floor(rng() * DECO_SETS.length)],
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -15,7 +15,13 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
troika: ["troika-three-text", "troika-worker-utils"],
|
three: [
|
||||||
|
"three",
|
||||||
|
"@react-three/fiber",
|
||||||
|
"@react-three/drei",
|
||||||
|
"troika-three-text",
|
||||||
|
"troika-worker-utils",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user