Fix mutate, added 404 & puzzle double sort
This commit is contained in:
parent
f654936e45
commit
e962fab69b
6 changed files with 155 additions and 52 deletions
|
@ -3,10 +3,20 @@ import Image from 'next/image';
|
|||
import error404 from '@/public/assets/404.png';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="m-auto flex h-screen flex-col items-center justify-center space-y-6">
|
||||
<h2 className="text-6xl">Oh non! Un François 404</h2>
|
||||
<Image priority src={error404} alt="François 404" width={1000} height={1000} />
|
||||
</div>
|
||||
);
|
||||
const random = Math.floor(Math.random() * 100);
|
||||
|
||||
if (random > 50) {
|
||||
return (
|
||||
<div className="m-auto flex h-screen flex-col items-center justify-center space-y-6">
|
||||
<h1 className="text-4xl">Oh non! Un François 404</h1>
|
||||
<Image priority src={error404} alt="François 404" width={800} height={800} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="m-auto flex h-screen flex-col items-center justify-center space-y-6">
|
||||
<h1 className="text-4xl">Oh non! Ce puzzle est introuvable</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,15 @@ export async function generateMetadata({ params }: { params: { id: number } }):
|
|||
const puzzle = await getPuzzle({ token, id });
|
||||
|
||||
if (!puzzle) {
|
||||
notFound();
|
||||
return {
|
||||
title: 'Puzzle introuvable',
|
||||
openGraph: {
|
||||
title: 'Puzzle introuvable',
|
||||
type: 'website',
|
||||
url: getURL(`/dashboard/puzzles/${id}`)
|
||||
// IMAGES WITH OG IMAGE
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,12 +5,12 @@ export const getChapters = async ({ token }: { token: string }): Promise<Chapter
|
|||
}
|
||||
});
|
||||
|
||||
const chapters = (await res.json()) as Chapter[];
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch puzzles');
|
||||
return [];
|
||||
}
|
||||
|
||||
const chapters = (await res.json()) as Chapter[];
|
||||
|
||||
if (!chapters) {
|
||||
return [];
|
||||
}
|
||||
|
@ -31,11 +31,10 @@ export const getChapter = async ({
|
|||
}
|
||||
});
|
||||
|
||||
const chapter = (await res.json()) as Chapter;
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch puzzles');
|
||||
return null;
|
||||
}
|
||||
const chapter = (await res.json()) as Chapter;
|
||||
|
||||
if (!chapter) {
|
||||
return null;
|
||||
|
@ -74,12 +73,12 @@ export const getPuzzle = async ({
|
|||
}
|
||||
});
|
||||
|
||||
const puzzle = (await res.json()) as Puzzle;
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch puzzle');
|
||||
return null;
|
||||
}
|
||||
|
||||
const puzzle = (await res.json()) as Puzzle;
|
||||
|
||||
if (!puzzle) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -62,8 +62,9 @@ export default function Puzzle({ token, id }: { token: string; id: number }) {
|
|||
|
||||
if (res.ok || res.status === 406 || res.status === 423) {
|
||||
const data = res.ok || res.status === 406 ? ((await res.json()) as Granted) : null;
|
||||
if (data && data.score) mutate(`puzzles/${puzzle?.id}`);
|
||||
else if (data && data.tries) setGranted(data);
|
||||
if (data && data.score) {
|
||||
mutate([`puzzles/${puzzle?.id}`, 'me']);
|
||||
} else if (data && data.tries) setGranted(data);
|
||||
else if (res.ok && data?.success)
|
||||
setGranted({ tries: null, score: null, message: 'Réponse correcte' });
|
||||
else if (res.status === 423)
|
||||
|
|
151
ui/Puzzles.tsx
151
ui/Puzzles.tsx
|
@ -19,11 +19,20 @@ import type { Chapter, Puzzle } from '@/lib/puzzles';
|
|||
import { cn } from '@/lib/utils';
|
||||
import useLocalStorage from '@/lib/hooks/use-local-storage';
|
||||
|
||||
const difficulty = [
|
||||
{ value: 'easy', label: 'Facile' },
|
||||
{ value: 'medium', label: 'Moyen' },
|
||||
{ value: 'hard', label: 'Difficile' }
|
||||
];
|
||||
|
||||
// TODO: REFACTOR FILTER TO AVOID WARNINGS
|
||||
|
||||
export default function Puzzles({ token }: { token: string }) {
|
||||
const { data: me } = useContext(UserContext);
|
||||
const { data, isLoading } = usePuzzles({ token });
|
||||
const [isOpen, setIsOpen] = useState<boolean[]>([]);
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [filterTags, setFilterTags] = useState<string>('');
|
||||
const [filterDifficulty, setFilterDifficulty] = useState<string>('');
|
||||
const [filterChapter, setFilterChapter] = useState<number>();
|
||||
|
||||
function handleClick(index: number) {
|
||||
|
@ -52,7 +61,7 @@ export default function Puzzles({ token }: { token: string }) {
|
|||
|
||||
if (now > startDate) {
|
||||
const minutes = 10 * 60 * 1000;
|
||||
if (startDate.getTime() + minutes < now.getTime()) {
|
||||
if (now.getTime() - startDate.getTime() < minutes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -61,25 +70,39 @@ export default function Puzzles({ token }: { token: string }) {
|
|||
}
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (filter && filterChapter) {
|
||||
if (filter === 'completed' || filter === 'no-completed') {
|
||||
return data
|
||||
?.find((chapter) => chapter.id === filterChapter)
|
||||
?.puzzles.filter((puzzle) => {
|
||||
if (filter === 'completed') {
|
||||
return puzzle!.score;
|
||||
}
|
||||
return !puzzle!.score;
|
||||
})
|
||||
.map((puzzle) => puzzle);
|
||||
}
|
||||
if ((filterTags || filterDifficulty) && filterChapter) {
|
||||
return data
|
||||
?.find((chapter) => chapter.id === filterChapter)
|
||||
?.puzzles.filter((puzzle) => puzzle!.tags!.some((tag) => tag.name === filter))
|
||||
?.puzzles.filter((puzzle) => {
|
||||
if (!puzzle?.tags) return false;
|
||||
if (filterDifficulty && filterTags) {
|
||||
if (filterTags === 'completed') {
|
||||
return puzzle!.tags!.some((tag) => tag.name === filterDifficulty) && puzzle!.score;
|
||||
} else if (filterTags === 'not-completed') {
|
||||
return puzzle!.tags!.some((tag) => tag.name === filterDifficulty) && !puzzle!.score;
|
||||
}
|
||||
return (
|
||||
puzzle!.tags!.some((tag) => tag.name === filterTags) &&
|
||||
puzzle!.tags!.some((tag) => tag.name === filterDifficulty)
|
||||
);
|
||||
}
|
||||
if (filterDifficulty) {
|
||||
return puzzle!.tags!.some((tag) => tag.name === filterDifficulty);
|
||||
}
|
||||
if (filterTags) {
|
||||
if (filterTags === 'completed') {
|
||||
return puzzle!.score;
|
||||
} else if (filterTags === 'not-completed') {
|
||||
return !puzzle!.score;
|
||||
}
|
||||
return puzzle!.tags!.some((tag) => tag.name === filterTags);
|
||||
}
|
||||
return puzzle;
|
||||
})
|
||||
.map((puzzle) => puzzle);
|
||||
}
|
||||
return data?.find((chapter) => chapter.id === filterChapter)?.puzzles;
|
||||
}, [data, filter, filterChapter]);
|
||||
}, [data, filterTags, filterDifficulty, filterChapter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -109,7 +132,7 @@ export default function Puzzles({ token }: { token: string }) {
|
|||
</div>
|
||||
<div className="flex flex-col">
|
||||
{chapter.startDate && chapter.endDate ? (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center justify-start gap-x-2 md:justify-end">
|
||||
<Icon name="calendar-line" className="text-sm text-muted" />
|
||||
<span className="text-sm text-muted">
|
||||
{new Date(chapter.startDate).toLocaleDateString('fr-FR', {
|
||||
|
@ -134,15 +157,24 @@ export default function Puzzles({ token }: { token: string }) {
|
|||
<div className="h-1 w-1/2 rounded-lg bg-gradient-to-tl from-brand to-brand-accent" />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1 flex justify-end">
|
||||
<div className="mt-1 flex justify-start gap-x-2 md:justify-end">
|
||||
{isInEventGroup(chapter) && (
|
||||
<FilterChapter
|
||||
chapters={data}
|
||||
chapter={chapter}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
setFilterChapter={setFilterChapter}
|
||||
/>
|
||||
<>
|
||||
<FilterDifficulty
|
||||
chapters={data}
|
||||
chapter={chapter}
|
||||
filter={filterDifficulty}
|
||||
setFilter={setFilterDifficulty}
|
||||
setFilterChapter={setFilterChapter}
|
||||
/>
|
||||
<FilterTags
|
||||
chapters={data}
|
||||
chapter={chapter}
|
||||
filter={filterTags}
|
||||
setFilter={setFilterTags}
|
||||
setFilterChapter={setFilterChapter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -202,10 +234,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
|||
const now = new Date();
|
||||
|
||||
if (now > startDate) {
|
||||
const minutes = 10 * 60 * 1000;
|
||||
if (startDate.getTime() + minutes < now.getTime()) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -257,7 +286,10 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
|||
<div className="flex h-full w-full items-center justify-between p-4 opacity-50">
|
||||
<div className="flex gap-x-2">
|
||||
<span className="text-base font-semibold">
|
||||
{puzzle.name} ({puzzle.scoreMax})
|
||||
{puzzle.name}{' '}
|
||||
<span className="text-sm text-highlight-secondary">
|
||||
({puzzle.score ? `${puzzle.score}` : '?'}/{puzzle.scoreMax} points)
|
||||
</span>
|
||||
</span>
|
||||
{puzzle.tags?.length && (
|
||||
<div className="flex gap-x-2 text-sm text-muted">
|
||||
|
@ -280,7 +312,7 @@ function PuzzleProp({ puzzle, chapter }: { puzzle: Puzzle; chapter: Chapter }) {
|
|||
);
|
||||
}
|
||||
|
||||
function FilterChapter({
|
||||
function FilterDifficulty({
|
||||
chapters,
|
||||
chapter,
|
||||
filter,
|
||||
|
@ -294,7 +326,7 @@ function FilterChapter({
|
|||
setFilterChapter: (chapter: number) => void;
|
||||
}) {
|
||||
const [stored, setStored] = useLocalStorage({
|
||||
key: 'puzzles-filter',
|
||||
key: 'filter-difficulty',
|
||||
initialValue: ''
|
||||
});
|
||||
|
||||
|
@ -304,12 +336,65 @@ function FilterChapter({
|
|||
.find((c) => c.id === chapter.id)
|
||||
?.puzzles?.map((p) => p.tags)
|
||||
.flat()
|
||||
.filter((tag) => difficulty.some((d) => tag?.name === d.value))
|
||||
.filter((tag, index, self) => self.findIndex((t) => t!.name === tag!.name) === index)
|
||||
.map((t) => {
|
||||
return { title: t!.name, value: t!.name };
|
||||
}) as { title: string; value: string }[];
|
||||
|
||||
options?.unshift({ title: 'Pas encore terminé(s)', value: 'no-completed' });
|
||||
options?.unshift({ title: 'Toutes les difficultés', value: '' });
|
||||
|
||||
setFilterChapter(chapter.id);
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
|
||||
setFilter(event.target.value);
|
||||
// TODO OPTI
|
||||
// @ts-ignore
|
||||
setStored(event.target.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (stored) {
|
||||
// TODO OPTI
|
||||
// @ts-ignore
|
||||
setFilter(stored);
|
||||
}
|
||||
}, [stored]);
|
||||
|
||||
return <Select className="w-44" options={options} value={filter} onChange={handleChange} />;
|
||||
}
|
||||
|
||||
function FilterTags({
|
||||
chapters,
|
||||
chapter,
|
||||
filter,
|
||||
setFilter,
|
||||
setFilterChapter
|
||||
}: {
|
||||
chapters: Chapter[];
|
||||
chapter: Chapter;
|
||||
filter: string;
|
||||
setFilter: (filter: string) => void;
|
||||
setFilterChapter: (chapter: number) => void;
|
||||
}) {
|
||||
const [stored, setStored] = useLocalStorage({
|
||||
key: 'filter-tags',
|
||||
initialValue: ''
|
||||
});
|
||||
|
||||
let options = [] as { title: string; value: string }[];
|
||||
|
||||
options = chapters
|
||||
.find((c) => c.id === chapter.id)
|
||||
?.puzzles?.map((p) => p.tags)
|
||||
.flat()
|
||||
.filter((tag) => !difficulty.some((d) => tag?.name === d.value))
|
||||
.filter((tag, index, self) => self.findIndex((t) => t!.name === tag!.name) === index)
|
||||
.map((t) => {
|
||||
return { title: t!.name, value: t!.name };
|
||||
}) as { title: string; value: string }[];
|
||||
|
||||
options?.unshift({ title: 'Pas encore terminé(s)', value: 'not-completed' });
|
||||
options?.unshift({ title: 'Terminé(s)', value: 'completed' });
|
||||
options?.unshift({ title: 'Tout les puzzles', value: '' });
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ const Select = forwardRef<
|
|||
ref={ref}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value} selected={option.value === props.value}>
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.title}
|
||||
</option>
|
||||
))}
|
||||
|
|
Loading…
Add table
Reference in a new issue