Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
11 changes: 5 additions & 6 deletions client/web/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import "./index.css";

import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";

Expand All @@ -13,9 +12,9 @@ import { router } from "./routes";
initSuperTokens();

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Providers>
<RouterProvider router={router} />
</Providers>
</React.StrictMode>,
// <React.StrictMode>
<Providers>
<RouterProvider router={router} />
</Providers>,
/* </React.StrictMode>, */
);
35 changes: 14 additions & 21 deletions client/web/src/pages/admin/reviews/ReviewsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,33 +277,18 @@ export default function ReviewsPage() {
);
}

// --- Loading state ---
if (loading && reviews.length === 0) {
return (
<div className="flex flex-1 min-h-0">
<Card className="overflow-hidden flex flex-col h-full w-full">
<CardHeader className="shrink-0 flex flex-row items-center justify-between">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-8 w-28 rounded-md" />
</CardHeader>
<CardContent className="p-0 flex-1 space-y-3 px-6 pb-6">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
</div>
);
}

return (
<div className="flex flex-1 min-h-0">
<Card
className={`overflow-hidden flex flex-col h-full ${selectedId ? "w-1/2 rounded-r-none" : "w-full"}`}
>
<CardHeader className="shrink-0 flex flex-row items-center pb-2 justify-between">
<div className="flex items-center gap-4">
<ReviewsTabToggle activeTab={tab} onTabChange={handleTabChange} />
<ReviewsTabToggle
activeTab={tab}
onTabChange={handleTabChange}
disabled={loading}
/>
<CardDescription className="font-light">
{description}
</CardDescription>
Expand All @@ -312,7 +297,15 @@ export default function ReviewsPage() {
</CardHeader>
<hr className="border-border -mb-2" />
<CardContent className="p-0 flex-1 overflow-hidden">
{table}
{loading && reviews.length === 0 ? (
<div className="space-y-3 p-6 pt-4">
{[...Array(6)].map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</div>
) : (
table
)}
</CardContent>
</Card>
{detailPanel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@ import type { ReviewTab } from "../store";
interface ReviewsTabToggleProps {
activeTab: ReviewTab;
onTabChange: (tab: ReviewTab) => void;
disabled?: boolean;
}

export const ReviewsTabToggle = memo(function ReviewsTabToggle({
activeTab,
onTabChange,
disabled,
}: ReviewsTabToggleProps) {
return (
<Tabs
value={activeTab}
onValueChange={(value) => onTabChange(value as ReviewTab)}
onValueChange={(value) => !disabled && onTabChange(value as ReviewTab)}
>
<TabsList className="h-9 rounded-md border gap-0 p-0.5">
<TabsTrigger
value="assigned"
className="font-light cursor-pointer rounded-sm"
disabled={disabled}
className="font-light cursor-pointer rounded-sm disabled:pointer-events-none disabled:opacity-50"
>
Assigned
</TabsTrigger>
<TabsTrigger
value="completed"
className="font-light cursor-pointer rounded-sm"
disabled={disabled}
className="font-light cursor-pointer rounded-sm disabled:pointer-events-none disabled:opacity-50"
>
Completed
</TabsTrigger>
Expand Down
4 changes: 2 additions & 2 deletions client/web/src/pages/admin/reviews/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ export const useReviewsStore = create<ReviewsState>((set, get) => ({
submitting: false,

setTab: (tab: ReviewTab) => {
set({ tab, reviews: [] });
set({ tab });
},

fetchReviews: async (signal?: AbortSignal) => {
set({ loading: true });
set({ loading: true, reviews: [] });

const { tab } = get();
const res =
Expand Down
54 changes: 7 additions & 47 deletions client/web/src/pages/admin/scans/components/ScanTypesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import {
Gift,
Loader2,
MoreHorizontal,
Pencil,
Plus,
ScanLine,
Trash2,
UserCheck,
Utensils,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
Expand Down Expand Up @@ -41,50 +38,13 @@ import {
} from "@/components/ui/table";

import type { ScanStat, ScanType, ScanTypeCategory } from "../types";

function toSnakeCase(str: string): string {
return str
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_|_$/g, "");
}

function validate(types: ScanType[]): string | null {
if (types.some((st) => !st.display_name.trim() || !st.name.trim())) {
return "All scan types must have a name";
}
const names = types.map((st) => st.name.trim());
if (new Set(names).size !== names.length) {
return "Scan type names must be unique";
}
const checkInCount = types.filter((st) => st.category === "check_in").length;
if (checkInCount !== 1) {
return "Exactly one scan type must have the check_in category";
}
return null;
}

const categoryIcons: Record<ScanTypeCategory, typeof UserCheck> = {
check_in: UserCheck,
meal: Utensils,
swag: Gift,
other: MoreHorizontal,
};

const categoryColors: Record<ScanTypeCategory, string> = {
check_in: "bg-blue-100 text-blue-800",
meal: "bg-orange-100 text-orange-800",
swag: "bg-purple-100 text-purple-800",
other: "bg-gray-100 text-gray-800",
};

const categoryOptions = [
{ value: "check_in", label: "Check In" },
{ value: "meal", label: "Meal" },
{ value: "swag", label: "Swag" },
{ value: "other", label: "Other" },
] as const;
import {
categoryColors,
categoryIcons,
categoryOptions,
toSnakeCase,
validate,
} from "../utils";

interface ScanTypesTableProps {
scanTypes: ScanType[];
Expand Down
47 changes: 47 additions & 0 deletions client/web/src/pages/admin/scans/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Gift, MoreHorizontal, UserCheck, Utensils } from "lucide-react";

import type { ScanType, ScanTypeCategory } from "./types";

export function toSnakeCase(str: string): string {
return str
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_|_$/g, "");
}

export function validate(types: ScanType[]): string | null {
if (types.some((st) => !st.display_name.trim() || !st.name.trim())) {
return "All scan types must have a name";
}
const names = types.map((st) => st.name.trim());
if (new Set(names).size !== names.length) {
return "Scan type names must be unique";
}
const checkInCount = types.filter((st) => st.category === "check_in").length;
if (checkInCount !== 1) {
return "Exactly one scan type must have the check_in category";
}
return null;
}

export const categoryIcons: Record<ScanTypeCategory, typeof UserCheck> = {
check_in: UserCheck,
meal: Utensils,
swag: Gift,
other: MoreHorizontal,
};

export const categoryColors: Record<ScanTypeCategory, string> = {
check_in: "bg-blue-100 text-blue-800",
meal: "bg-orange-100 text-orange-800",
swag: "bg-purple-100 text-purple-800",
other: "bg-gray-100 text-gray-800",
};

export const categoryOptions = [
{ value: "check_in", label: "Check In" },
{ value: "meal", label: "Meal" },
{ value: "swag", label: "Swag" },
{ value: "other", label: "Other" },
] as const;
93 changes: 16 additions & 77 deletions client/web/src/pages/superadmin/application/ApplicationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Loader2, Plus, Save, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { useEffect } from "react";

import {
AlertDialog,
Expand All @@ -23,87 +22,27 @@ import {
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { errorAlert, getRequest, putRequest } from "@/shared/lib/api";
import type { ShortAnswerQuestion } from "@/types";

import { ApplicationPreview } from "./components/ApplicationPreview";
import { useApplicationSettingsStore } from "./store";

export default function ApplicationPage() {
const [questions, setQuestions] = useState<ShortAnswerQuestion[]>([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const {
questions,
loading,
saving,
fetchQuestions,
saveQuestions,
updateQuestion,
addQuestion,
removeQuestion,
} = useApplicationSettingsStore();

useEffect(() => {
const fetchQuestions = async () => {
setLoading(true);
const res = await getRequest<{ questions: ShortAnswerQuestion[] }>(
"/superadmin/settings/saquestions",
"short answer questions",
);
if (res.status === 200 && res.data) {
setQuestions(res.data.questions ?? []);
} else {
errorAlert(res);
}
setLoading(false);
};
fetchQuestions();
}, []);

const saveQuestions = useCallback(async () => {
const emptyQuestion = questions.find((q) => !q.question.trim());
if (emptyQuestion) {
toast.error("All questions must have text before saving");
return;
}

setSaving(true);
const payload = questions.map((q, i) => ({ ...q, display_order: i + 1 }));
const res = await putRequest<{ questions: ShortAnswerQuestion[] }>(
"/superadmin/settings/saquestions",
{ questions: payload },
"short answer questions",
);
if (res.status === 200 && res.data) {
toast.success("Questions saved");
} else {
errorAlert(res);
}
setSaving(false);
}, [questions]);

const updateQuestions = useCallback(
(updater: (prev: ShortAnswerQuestion[]) => ShortAnswerQuestion[]) => {
setQuestions((prev) => updater(prev));
},
[],
);

const updateQuestion = (
index: number,
field: keyof ShortAnswerQuestion,
value: string | boolean | number,
) => {
updateQuestions((prev) =>
prev.map((q, i) => (i === index ? { ...q, [field]: value } : q)),
);
};

const addQuestion = () => {
updateQuestions((prev) => [
...prev,
{
id: `saq_${Date.now()}`,
question: "",
required: false,
display_order: prev.length + 1,
},
]);
};

const removeQuestion = (index: number) => {
updateQuestions((prev) => prev.filter((_, i) => i !== index));
};
const controller = new AbortController();
fetchQuestions(controller.signal);
return () => controller.abort();
}, [fetchQuestions]);

return (
<div className="flex flex-1 min-h-0">
Expand Down
26 changes: 26 additions & 0 deletions client/web/src/pages/superadmin/application/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getRequest, putRequest } from "@/shared/lib/api";
import type { ApiResponse, ShortAnswerQuestion } from "@/types";

interface SAQuestionsResponse {
questions: ShortAnswerQuestion[];
}

export async function fetchSAQuestions(
signal?: AbortSignal,
): Promise<ApiResponse<SAQuestionsResponse>> {
return getRequest<SAQuestionsResponse>(
"/superadmin/settings/saquestions",
"short answer questions",
signal,
);
}

export async function saveSAQuestions(
questions: ShortAnswerQuestion[],
): Promise<ApiResponse<SAQuestionsResponse>> {
return putRequest<SAQuestionsResponse>(
"/superadmin/settings/saquestions",
{ questions },
"short answer questions",
);
}
Loading
Loading