All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m17s
199 lines
6.8 KiB
TypeScript
199 lines
6.8 KiB
TypeScript
import { type ClassValue, clsx } from "clsx";
|
|
import { del } from "idb-keyval";
|
|
import { DateTime } from "luxon";
|
|
import type { NavigateFunction } from "react-router";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { CACHE_KEY } from "./client";
|
|
import type { User, UserPreferences } from "./api";
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
|
|
/**
|
|
* === STRING UTILS ===
|
|
*/
|
|
export function capitalizeFirstLetter(str: string): string {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
|
|
export function titleCase(str: string) {
|
|
return str.replace(
|
|
/\w\S*/g,
|
|
(text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* === NAVIGATION UTILS ===
|
|
*/
|
|
// Force a reload of the page
|
|
export function forceReload() {
|
|
window.location.reload();
|
|
}
|
|
|
|
export function goBack(navigate: NavigateFunction) {
|
|
let canGoBack = false;
|
|
// Check if there is a history stack to go back to
|
|
try {
|
|
canGoBack = window.history.length > 1;
|
|
} catch (e) {
|
|
canGoBack = false;
|
|
}
|
|
if (!canGoBack) {
|
|
return navigate("/");
|
|
} else {
|
|
return navigate(-1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* === COLLES UTILS ===
|
|
*/
|
|
export const formatDate = (date: string) => {
|
|
const dt = DateTime.fromISO(date).setLocale("fr");
|
|
const str = dt.toLocaleString({
|
|
weekday: "long",
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
return titleCase(str);
|
|
};
|
|
|
|
export const formatTime = (date: string) => {
|
|
const dt = DateTime.fromISO(date).setLocale("fr");
|
|
return (
|
|
dt
|
|
.toLocaleString({
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})
|
|
?.replace(":", "h") || "N/A"
|
|
);
|
|
};
|
|
|
|
export const formatGrade = (grade?: number) => {
|
|
if (grade === undefined || grade === null || grade < 0 || grade > 20)
|
|
return "N/A";
|
|
|
|
const rounded = Math.round(grade * 10) / 10;
|
|
const str =
|
|
rounded % 1 === 0
|
|
? rounded.toFixed(0) // no decimals if .0
|
|
: rounded.toFixed(1); // one decimal otherwise
|
|
|
|
return str.replace(".", ",").padStart(2, "0"); // pad with zero if needed
|
|
};
|
|
|
|
// === COLORS UTILS ===
|
|
export const colors = [
|
|
"red",
|
|
"orange",
|
|
"amber",
|
|
"yellow",
|
|
"lime",
|
|
"green",
|
|
"emerald",
|
|
"teal",
|
|
"cyan",
|
|
"sky",
|
|
"blue",
|
|
"indigo",
|
|
"violet",
|
|
"purple",
|
|
"fuchsia",
|
|
"pink",
|
|
"rose",
|
|
"slate",
|
|
"zinc",
|
|
];
|
|
|
|
export const getColorClass = (colorCode: string) =>
|
|
`bg-${colorCode}-100 text-${colorCode}-800 dark:bg-${colorCode}-300 dark:text-${colorCode}-900 hover:bg-${colorCode}-100 dark:hover:bg-${colorCode}-300`;
|
|
|
|
// Used for Tailwind CSS to generate the classes
|
|
export const classNames = [
|
|
[
|
|
"bg-red-100 text-red-800 dark:bg-red-300 dark:text-red-900 hover:bg-red-100 dark:hover:bg-red-300",
|
|
"bg-orange-100 text-orange-800 dark:bg-orange-300 dark:text-orange-900 hover:bg-orange-100 dark:hover:bg-orange-300",
|
|
"bg-amber-100 text-amber-800 dark:bg-amber-300 dark:text-amber-900 hover:bg-amber-100 dark:hover:bg-amber-300",
|
|
"bg-yellow-100 text-yellow-800 dark:bg-yellow-300 dark:text-yellow-900 hover:bg-yellow-100 dark:hover:bg-yellow-300",
|
|
"bg-lime-100 text-lime-800 dark:bg-lime-300 dark:text-lime-900 hover:bg-lime-100 dark:hover:bg-lime-300",
|
|
"bg-green-100 text-green-800 dark:bg-green-300 dark:text-green-900 hover:bg-green-100 dark:hover:bg-green-300",
|
|
"bg-emerald-100 text-emerald-800 dark:bg-emerald-300 dark:text-emerald-900 hover:bg-emerald-100 dark:hover:bg-emerald-300",
|
|
"bg-teal-100 text-teal-800 dark:bg-teal-300 dark:text-teal-900 hover:bg-teal-100 dark:hover:bg-teal-300",
|
|
"bg-cyan-100 text-cyan-800 dark:bg-cyan-300 dark:text-cyan-900 hover:bg-cyan-100 dark:hover:bg-cyan-300",
|
|
"bg-sky-100 text-sky-800 dark:bg-sky-300 dark:text-sky-900 hover:bg-sky-100 dark:hover:bg-sky-300",
|
|
"bg-blue-100 text-blue-800 dark:bg-blue-300 dark:text-blue-900 hover:bg-blue-100 dark:hover:bg-blue-300",
|
|
"bg-indigo-100 text-indigo-800 dark:bg-indigo-300 dark:text-indigo-900 hover:bg-indigo-100 dark:hover:bg-indigo-300",
|
|
"bg-violet-100 text-violet-800 dark:bg-violet-300 dark:text-violet-900 hover:bg-violet-100 dark:hover:bg-violet-300",
|
|
"bg-purple-100 text-purple-800 dark:bg-purple-300 dark:text-purple-900 hover:bg-purple-100 dark:hover:bg-purple-300",
|
|
"bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-300 dark:text-fuchsia-900 hover:bg-fuchsia-100 dark:hover:bg-fuchsia-300",
|
|
"bg-pink-100 text-pink-800 dark:bg-pink-300 dark:text-pink-900 hover:bg-pink-100 dark:hover:bg-pink-300",
|
|
"bg-rose-100 text-rose-800 dark:bg-rose-300 dark:text-rose-900 hover:bg-rose-100 dark:hover:bg-rose-300",
|
|
"bg-slate-100 text-slate-800 dark:bg-slate-300 dark:text-slate-900 hover:bg-slate-100 dark:hover:bg-slate-300",
|
|
"bg-zinc-100 text-zinc-800 dark:bg-zinc-300 dark:text-zinc-900 hover:bg-zinc-100 dark:hover:bg-zinc-300",
|
|
"bg-gray-100 text-gray-800 dark:bg-gray-300 dark:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-300",
|
|
],
|
|
];
|
|
|
|
export const DEFAULT_COLOR = "blue"; // Default color for subjects
|
|
export const DEFAULT_EMOJI = "📚"; // Default emoji for subjects
|
|
|
|
export const getSubjectEmoji = (subject: string, pref: UserPreferences) => {
|
|
const preference = pref.find((p) => p.name === subject);
|
|
return preference ? preference.emoji : DEFAULT_EMOJI;
|
|
};
|
|
export const getSubjectColor = (subject: string, pref: UserPreferences) => {
|
|
const preference = pref.find((p) => p.name === subject);
|
|
return preference ? preference.color : DEFAULT_COLOR;
|
|
};
|
|
|
|
// === DEBUG UTILS ===
|
|
export const clearCache = () => del(CACHE_KEY);
|
|
|
|
// === MP* COLLES SYLLABUS CONSTANTS ===
|
|
export function getColleWeek(date: Date) {
|
|
// Define week ranges [start, end, week_number]
|
|
const weeks = [
|
|
["15/09/2025", "20/09/2025", 0],
|
|
["22/09/2025", "27/09/2025", 1],
|
|
["29/09/2025", "04/10/2025", 2],
|
|
["06/10/2025", "11/10/2025", 3],
|
|
["13/10/2025", "18/10/2025", 4],
|
|
["03/11/2025", "08/11/2025", 5],
|
|
["10/11/2025", "15/11/2025", 6],
|
|
["17/11/2025", "22/11/2025", 7],
|
|
["24/11/2025", "29/11/2025", 8],
|
|
["01/12/2025", "06/12/2025", 9],
|
|
["08/12/2025", "13/12/2025", 10],
|
|
["15/12/2025", "20/12/2025", 11],
|
|
["05/01/2026", "10/01/2026", 12],
|
|
["12/01/2026", "17/01/2026", 13],
|
|
["19/01/2026", "24/01/2026", 14],
|
|
["26/01/2026", "31/01/2026", 15],
|
|
["02/02/2026", "07/02/2026", 16],
|
|
["09/02/2026", "14/02/2026", 17],
|
|
["09/03/2026", "14/03/2026", 18],
|
|
["16/03/2026", "21/03/2026", 19],
|
|
] as [string, string, number][];
|
|
|
|
// Helper function to parse DD/MM/YYYY string to Date
|
|
function parseDate(dateStr: string): Date {
|
|
const [day, month, year] = dateStr.split("/").map(Number);
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
|
|
// Check which week the date falls into
|
|
for (const [startStr, endStr, weekNum] of weeks) {
|
|
const startDate = parseDate(startStr);
|
|
const endDate = parseDate(endStr);
|
|
|
|
if (date >= startDate && date <= endDate) {
|
|
return weekNum;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|