-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from timhabermaas/move-records-page-to-nextjs
Move records page to nextjs
- Loading branch information
Showing
25 changed files
with
1,097 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
const WINDOW_SIZE = 3; | ||
|
||
/** | ||
* Returns the page numbers to display in the pagination component. | ||
* | ||
* @param current the currently selected page | ||
* @param total the total number of pages | ||
* @returns An array of consecutive page numbers with `null` representing gaps | ||
*/ | ||
export function pages(current: number, total: number): (number[] | null)[] { | ||
if (total <= WINDOW_SIZE) { | ||
return [new Array(total).fill(null).map((_, i) => i + 1)]; | ||
} | ||
|
||
let start = current - WINDOW_SIZE; | ||
let end = current + WINDOW_SIZE; | ||
|
||
if (start < 1) { | ||
start = 1; | ||
} | ||
|
||
if (end > total) { | ||
end = total; | ||
} | ||
|
||
let middleWindow = new Array(end - start + 1) | ||
.fill(null) | ||
.map((_, i) => i + start); | ||
|
||
let startWindow = [1, 2]; | ||
let endWindow = [total - 1, total]; | ||
|
||
let result: (number[] | null)[]; | ||
|
||
if (startWindow[startWindow.length - 1] + 1 >= middleWindow[0]) { | ||
if (endWindow[0] - 1 <= middleWindow[middleWindow.length - 1]) { | ||
result = [makeUnique(startWindow.concat(middleWindow).concat(endWindow))]; | ||
} else { | ||
result = [makeUnique(startWindow.concat(middleWindow)), null, endWindow]; | ||
} | ||
} else { | ||
if (endWindow[0] - 1 <= middleWindow[middleWindow.length - 1]) { | ||
result = [startWindow, null, makeUnique(middleWindow.concat(endWindow))]; | ||
} else { | ||
result = [startWindow, null, middleWindow, null, endWindow]; | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function makeUnique<T>(list: T[]): T[] { | ||
return Array.from(new Set(list)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/** Formats a time given in ISO8601 format as a long day. | ||
* | ||
* e.g. "October 16, 2019" | ||
* | ||
* @param time the time given in ISO8601 | ||
*/ | ||
export function formatDate(time: string): string { | ||
return new Date(time).toLocaleDateString(undefined, { | ||
year: "numeric", | ||
month: "long", | ||
day: "numeric", | ||
}); | ||
} | ||
|
||
/** Formats a given duration in ms to a duration using min and s. Doesn't display | ||
* the minutes if they are 0. | ||
* | ||
* 10000 => 10.00s | ||
* 60005 => 1:00.01s | ||
* | ||
* @param duration an integer containing the duration in ms | ||
* @returns a formatted duration, e.g. 2:03.52min | ||
*/ | ||
export function formatDuration(duration: number): string { | ||
let seconds = Math.round(duration / 10) / 100; | ||
if (seconds < 60) { | ||
return `${seconds.toFixed(2)}s`; | ||
} else { | ||
const minutes = Math.floor(seconds / 60); | ||
seconds -= minutes * 60; | ||
const s = seconds < 10 ? "0" : ""; | ||
return `${minutes}:${s}${seconds.toFixed(2)}min`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface PaginatedResponse<T> { | ||
items: Array<T>; | ||
page: number; | ||
next_page: number | null; | ||
total_item_count: number; | ||
max_items_per_page: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import Link from "next/link"; | ||
import React, { useEffect, useRef, useState } from "react"; | ||
import { usePuzzleFromUrl } from "../hooks/usePuzzleFromUrl"; | ||
import { Kind, usePuzzles } from "../hooks/usePuzzles"; | ||
|
||
function movePuzzles(puzzleEl: HTMLUListElement, index: number) { | ||
puzzleEl.setAttribute("style", `left: ${-100 * index}%; width: 400%;`); | ||
} | ||
|
||
function findPuzzleIndex(kind: Kind[], puzzleSlug: string): number { | ||
const result = kind.findIndex((k) => { | ||
return k.puzzles.find((p) => p.slug === puzzleSlug) !== undefined; | ||
}); | ||
|
||
return result < 0 ? 0 : result; | ||
} | ||
|
||
export function PuzzleNav() { | ||
const { data } = usePuzzles(); | ||
const currentPuzzleSlug = usePuzzleFromUrl(); | ||
|
||
const [currentKind, setCurrentKind] = useState<number>( | ||
findPuzzleIndex(data ?? [], currentPuzzleSlug) | ||
); | ||
|
||
const puzzlesRef = useRef<HTMLUListElement>(null); | ||
|
||
useEffect(() => { | ||
if (!data) { | ||
return; | ||
} | ||
|
||
let kind = findPuzzleIndex(data, currentPuzzleSlug); | ||
setCurrentKind(kind); | ||
|
||
if (puzzlesRef.current) { | ||
movePuzzles(puzzlesRef.current, kind); | ||
} | ||
}, [data, currentPuzzleSlug]); | ||
|
||
const onKindClick = (event: React.MouseEvent, i: number) => { | ||
event.preventDefault(); | ||
|
||
if (puzzlesRef.current) { | ||
movePuzzles(puzzlesRef.current, i); | ||
setCurrentKind(i); | ||
} | ||
}; | ||
|
||
if (data === undefined) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<nav id="subnavigation"> | ||
<div id="puzzles"> | ||
<ul ref={puzzlesRef} style={{ width: "400%", left: "0%" }}> | ||
{data.map((k) => ( | ||
<li key={k.id} style={{ width: `${100 / data.length}%` }}> | ||
<ul className="puzzles"> | ||
{k.puzzles.map((p) => ( | ||
<li | ||
key={p.id} | ||
className={p.slug === currentPuzzleSlug ? "checked" : ""} | ||
> | ||
<Link href={`/puzzles/${p.slug}/records`}> | ||
<a> | ||
<span className={`puzzle pos${p.css_position}`}> | ||
<span className={`kind pos${k.css_position}`}></span> | ||
</span> | ||
<span className="name">{p.name}</span> | ||
</a> | ||
</Link>{" "} | ||
</li> | ||
))} | ||
</ul> | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
<div id="kinds"> | ||
<ul className="center"> | ||
{data.map((kind, i) => ( | ||
<li | ||
key={kind.id} | ||
style={{ width: `${100 / data.length}%` }} | ||
className={i == currentKind ? "checked" : ""} | ||
> | ||
<a | ||
href="#" | ||
onClick={(e) => { | ||
onKindClick(e, i); | ||
}} | ||
> | ||
{kind.name} | ||
</a> | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
</nav> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useMutation, useQueryClient } from "react-query"; | ||
|
||
export function useBlockUser(jwtToken?: string) { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation( | ||
async (slug: string) => { | ||
const headers = new Headers(); | ||
if (jwtToken) { | ||
headers.set("Authorization", `Bearer ${jwtToken}`); | ||
} | ||
const r = await fetch(`/api/users/${slug}/block`, { | ||
headers, | ||
method: "PUT", | ||
}); | ||
return await r.json(); | ||
}, | ||
{ | ||
onSuccess: () => { | ||
queryClient.invalidateQueries("records"); | ||
}, | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,34 @@ | ||
import { useQuery } from "react-query"; | ||
import { useQuery, useQueryClient } from "react-query"; | ||
|
||
export type Role = "admin" | "moderator" | "user"; | ||
|
||
interface MeResponse { | ||
current_user?: { name: string; slug: string }; | ||
current_user?: { | ||
name: string; | ||
slug: string; | ||
role: Role; | ||
}; | ||
} | ||
|
||
export function useMe(jwtToken?: string) { | ||
return useQuery<MeResponse, Error>(["me"], async () => { | ||
const headers = new Headers(); | ||
if (jwtToken) { | ||
headers.set("Authorization", `Bearer ${jwtToken}`); | ||
const queryClient = useQueryClient(); | ||
|
||
return useQuery<MeResponse, Error>( | ||
["me"], | ||
async () => { | ||
const headers = new Headers(); | ||
if (jwtToken) { | ||
headers.set("Authorization", `Bearer ${jwtToken}`); | ||
} | ||
const r = await fetch("/api/me", { | ||
headers, | ||
}); | ||
return await r.json(); | ||
}, | ||
{ | ||
onSuccess: () => { | ||
queryClient.invalidateQueries("records"); | ||
}, | ||
} | ||
const r = await fetch("/api/me", { | ||
headers, | ||
}); | ||
return await r.json(); | ||
}); | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Role, useMe } from "./useMe"; | ||
|
||
export function useMyRole(jwtToken?: string): Role | undefined { | ||
const { data } = useMe(jwtToken); | ||
|
||
return data && data.current_user?.role; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { useSingleQueryParam } from "./useSingleQueryParam"; | ||
|
||
export function usePuzzleFromUrl(): string { | ||
const q = useSingleQueryParam("puzzleId"); | ||
if (q === null) { | ||
throw new Error("routing error"); | ||
} else { | ||
return q; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { useQuery } from "react-query"; | ||
|
||
export interface Puzzle { | ||
css_position: number; | ||
id: number; | ||
name: string; | ||
slug: string; | ||
} | ||
|
||
export interface Kind { | ||
id: number; | ||
name: string; | ||
css_position: number; | ||
puzzles: Puzzle[]; | ||
} | ||
|
||
export function usePuzzles() { | ||
return useQuery<Kind[]>( | ||
"allPuzzles", | ||
async () => { | ||
const response = await fetch("/api/puzzles"); | ||
return await response.json(); | ||
}, | ||
{ staleTime: Infinity } | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { RecordTypes } from "../models/recordTypes"; | ||
import { useSingleQueryParam } from "./useSingleQueryParam"; | ||
|
||
export function useRecordTypeFromUrl(): string { | ||
return useSingleQueryParam("type") ?? RecordTypes[1].short; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { useQuery } from "react-query"; | ||
import { PaginatedResponse } from "../commons/types/PaginatedResponse"; | ||
|
||
export interface Record { | ||
id: number; | ||
rank: number; | ||
time: number; | ||
comment: string; | ||
set_at: string; | ||
user_name: string; | ||
user_slug: string; | ||
} | ||
|
||
interface RecordResponse { | ||
records: PaginatedResponse<Record>; | ||
} | ||
|
||
export function useRecords(type: string, page: number, puzzleSlug: string) { | ||
return useQuery<RecordResponse>( | ||
["records", puzzleSlug, type, page], | ||
async () => { | ||
let response = await fetch( | ||
`/api/records?type=${type}&page=${page}&puzzle_slug=${puzzleSlug}` | ||
); | ||
return await response.json(); | ||
}, | ||
{ keepPreviousData: true } | ||
); | ||
} |
Oops, something went wrong.