diff --git a/.gitignore b/.gitignore index cad5a6cd..0cf3fd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ dist-ssr tmpMock.ts .python-version +.env diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index d84cd9b9..c2780f30 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -28,7 +28,7 @@ oidc = OpenIdConnect( - openIdConnectUrl="https://old.online.ntnu.no/openid/.well-known/openid-configuration", + openIdConnectUrl="https://auth.online.ntnu.no/openid/.well-known/openid-configuration", ) diff --git a/backend/app/api/endpoints/user.py b/backend/app/api/endpoints/user.py index dcc47517..7d3812b5 100644 --- a/backend/app/api/endpoints/user.py +++ b/backend/app/api/endpoints/user.py @@ -2,13 +2,15 @@ User endpoints """ +from app.models.punishment import LeaderboardPunishmentRead from fastapi import APIRouter, Depends, HTTPException, Query from app.api import APIRoute, Request, oidc from app.exceptions import NotFound from app.models.group import UserWithGroups -from app.models.user import LeaderboardUser +from app.models.user import MinifiedLeaderboardUser from app.utils.pagination import Page, Pagination +from app.types import UserId router = APIRouter( prefix="/users", @@ -61,14 +63,14 @@ async def get_me( @router.get( "/leaderboard", - response_model=Page[LeaderboardUser], + response_model=Page[MinifiedLeaderboardUser], dependencies=[Depends(oidc)], ) async def get_leadeboard( request: Request, page: int = Query(title="Page number", default=0, ge=0), page_size: int = Query(title="Page size", default=30, ge=1, le=50), -) -> Page[LeaderboardUser]: +) -> Page[MinifiedLeaderboardUser]: access_token = request.raise_if_missing_authorization() app = request.app @@ -81,11 +83,29 @@ async def get_leadeboard( status_code=403, detail="Du har ikke tilgang til denne ressursen" ) - pagination = Pagination[LeaderboardUser]( + pagination = Pagination[MinifiedLeaderboardUser]( request=request, total_coro=app.db.users.get_leaderboard_count, - results_coro=app.db.users.get_leaderboard, + # results_coro=app.db.users.get_leaderboard, + results_coro=app.db.users.get_minified_leaderboard, page=page, page_size=page_size, ) return await pagination.paginate(conn=conn) + +@router.get( + "/leaderboard/punishments/{user_id}", + response_model=list[LeaderboardPunishmentRead], + dependencies=[Depends(oidc)], +) +async def get_user_punishments( + request: Request, + user_id: UserId, +) -> list[LeaderboardPunishmentRead]: + access_token = request.raise_if_missing_authorization() + + app = request.app + _, _ = await app.ow_sync.sync_for_access_token(access_token) + + async with app.db.pool.acquire() as conn: + return await app.db.users.get_punishments_for_leaderboard_user(user_id, conn=conn) diff --git a/backend/app/api/init_api.py b/backend/app/api/init_api.py index 74cadc34..5587b667 100644 --- a/backend/app/api/init_api.py +++ b/backend/app/api/init_api.py @@ -111,7 +111,7 @@ async def shutdown_handler() -> None: def init_api(**db_settings: str) -> FastAPI: oauth = { - "clientId": "219919", + "clientId": "5rOMfB8Ztegz", "appName": "Vengeful Vineyard Docs", "usePkceWithAuthorizationCodeGrant": True, "scopes": "openid email profile onlineweb4", @@ -122,10 +122,6 @@ def init_api(**db_settings: str) -> FastAPI: swagger_ui_oauth2_redirect_url="/docs/oauth2-redirect", ) - @app.get("/crash") - async def crash(): - raise Exception("wow i crashed") - app.openapi_version = "3.0.0" app.router.route_class = APIRoute init_middlewares(app) diff --git a/backend/app/db/groups.py b/backend/app/db/groups.py index 9f959438..b9596851 100644 --- a/backend/app/db/groups.py +++ b/backend/app/db/groups.py @@ -314,6 +314,7 @@ async def insert_members( FROM unnest($1::group_members[]) as m ) + ON CONFLICT (group_id, user_id) DO NOTHING RETURNING * """ diff --git a/backend/app/db/users.py b/backend/app/db/users.py index 86b689e4..1f0b73f5 100644 --- a/backend/app/db/users.py +++ b/backend/app/db/users.py @@ -5,7 +5,8 @@ from app.exceptions import DatabaseIntegrityException, NotFound from app.models.group import Group -from app.models.user import LeaderboardUser, User, UserCreate, UserUpdate +from app.models.user import LeaderboardUser, MinifiedLeaderboardUser, User, UserCreate, UserUpdate +from app.models.punishment import LeaderboardPunishmentRead from app.types import InsertOrUpdateUser, OWUserId, UserId from app.utils.db import MaybeAcquire @@ -44,93 +45,6 @@ async def get_leaderboard_count( assert isinstance(res, int) return res - async def get_leaderboard( - self, - offset: int, - limit: int, - force_include_reasons: bool = False, - conn: Optional[Pool] = None, - ) -> list[LeaderboardUser]: - async with MaybeAcquire(conn, self.db.pool) as conn: - query = """ - WITH punishments_with_reactions AS ( - SELECT - gp.*, - COALESCE(NULLIF(u.first_name, ''), u.email) || ' ' || u.last_name as created_by_name, - COALESCE(json_agg(json_build_object( - 'punishment_reaction_id', pr.punishment_reaction_id, - 'punishment_id', pr.punishment_id, - 'emoji', pr.emoji, - 'created_at', pr.created_at, - 'created_by', pr.created_by, - 'created_by_name', (SELECT COALESCE(NULLIF(first_name, ''), email) || ' ' || last_name FROM users WHERE user_id = pr.created_by) - )) FILTER (WHERE pr.punishment_reaction_id IS NOT NULL), '[]') as reactions - FROM group_punishments gp - LEFT JOIN punishment_reactions pr - ON pr.punishment_id = gp.punishment_id - LEFT JOIN users u - ON u.user_id = gp.created_by - LEFT JOIN groups g - ON g.group_id = gp.group_id - WHERE g.ow_group_id IS NOT NULL OR special - GROUP BY gp.punishment_id, created_by_name - ) - SELECT u.*, - COALESCE(json_agg( - json_build_object( - 'punishment_id', pwr.punishment_id, - 'user_id', pwr.user_id, - 'punishment_type_id', pwr.punishment_type_id, - 'reason', pwr.reason, - 'reason_hidden', pwr.reason_hidden, - 'amount', pwr.amount, - 'created_by', pwr.created_by, - 'created_by_name', pwr.created_by_name, - 'created_at', pwr.created_at, - 'group_id', pwr.group_id, - 'paid', pwr.paid, - 'paid_at', pwr.paid_at, - 'marked_paid_by', pwr.marked_paid_by, - 'reactions', pwr.reactions, - 'punishment_type', (SELECT json_build_object( - 'punishment_type_id', pt.punishment_type_id, - 'name', pt.name, - 'value', pt.value, - 'emoji', pt.emoji, - 'created_at', pt.created_at, - 'created_by', pt.created_by, - 'updated_at', pt.updated_at - ) FROM punishment_types pt WHERE pt.punishment_type_id = pwr.punishment_type_id) - ) - ) FILTER (WHERE pwr.punishment_id IS NOT NULL), '[]') AS punishments, - COALESCE(SUM(pwr.amount * pt.value), 0) as total_value - FROM users u - LEFT JOIN punishments_with_reactions pwr - ON pwr.user_id = u.user_id - LEFT JOIN punishment_types pt - ON pt.punishment_type_id = pwr.punishment_type_id - INNER JOIN groups g - ON g.group_id = pwr.group_id AND g.ow_group_id IS NOT NULL OR special - GROUP BY u.user_id - ORDER BY total_value DESC, u.first_name ASC - OFFSET $1 - LIMIT $2""" - res = await conn.fetch( - query, - offset, - limit, - ) - - users = [LeaderboardUser(**r) for r in res] - - if not force_include_reasons: - for user in users: - for punishment in user.punishments: - if punishment.reason_hidden: - punishment.reason = "" - - return users - async def get_all_raw( self, conn: Optional[Pool] = None, @@ -432,3 +346,95 @@ async def get_groups( result = await conn.fetch(query, user_id) return [Group(**row) for row in result] + + async def get_minified_leaderboard( + self, + offset: int, + limit: int, + conn: Optional[Pool] = None, + ) -> list[MinifiedLeaderboardUser]: + async with MaybeAcquire(conn, self.db.pool) as conn: + query = """ + SELECT + DISTINCT u.user_id, + u.first_name, + u.last_name, + u.email, + u.ow_user_id, + COALESCE(p.total_value, 0) AS total_value, + COALESCE(p.emojis, '') AS emojis, + COALESCE(p.amount_punishments, 0) AS amount_punishments, + COALESCE(p.amount_unique_punishments, 0) AS amount_unique_punishments + FROM users u + LEFT JOIN ( + SELECT + p.user_id, + SUM(pt.value * p.amount) AS total_value, + STRING_AGG(REPEAT(pt.emoji, p.amount), '') AS emojis, + SUM(p.amount) AS amount_punishments, + COUNT(DISTINCT p.punishment_type_id) AS amount_unique_punishments + FROM group_punishments p + LEFT JOIN punishment_types pt + ON pt.punishment_type_id = p.punishment_type_id + LEFT JOIN groups g + ON g.group_id = p.group_id + WHERE g.ow_group_id IS NOT NULL + GROUP BY p.user_id + ) p ON p.user_id = u.user_id + LEFT JOIN group_members gm + ON gm.user_id = u.user_id + LEFT JOIN groups g + ON g.group_id = gm.group_id + WHERE g.ow_group_id IS NOT NULL OR g.special + ORDER BY total_value DESC, u.first_name ASC + OFFSET $1 + LIMIT $2 + """ + res = await conn.fetch( + query, + offset, + limit, + ) + + return [MinifiedLeaderboardUser(**r) for r in res] + + async def get_punishments_for_leaderboard_user( + self, + user_id: UserId, + conn: Optional[Pool] = None, + ) -> list[LeaderboardPunishmentRead]: + async with MaybeAcquire(conn, self.db.pool) as conn: + query = """ + SELECT + gp.*, + CONCAT(COALESCE(NULLIF(users.first_name, ''), users.email), ' ', users.last_name) AS created_by_name, + COALESCE(json_agg(pr) FILTER (WHERE pr.punishment_reaction_id IS NOT NULL), '[]') as reactions, + json_build_object( + 'punishment_type_id', pt.punishment_type_id, + 'name', pt.name, + 'value', pt.value, + 'emoji', pt.emoji, + 'created_at', pt.created_at, + 'created_by', pt.created_by, + 'updated_at', pt.updated_at + ) AS punishment_type + FROM group_punishments gp + LEFT JOIN punishment_types pt + ON pt.punishment_type_id = gp.punishment_type_id + LEFT JOIN ( + SELECT pr1.* + FROM punishment_reactions pr1 + JOIN group_members gm ON pr1.created_by = gm.user_id + GROUP BY pr1.punishment_reaction_id + ) pr ON pr.punishment_id = gp.punishment_id + LEFT JOIN users ON gp.created_by = users.user_id + WHERE gp.user_id = $1 + GROUP BY gp.punishment_id, created_by_name, pt.punishment_type_id + ORDER BY gp.created_at DESC + """ + res = await conn.fetch( + query, + user_id, + ) + + return [LeaderboardPunishmentRead(**r) for r in res] diff --git a/backend/app/models/punishment.py b/backend/app/models/punishment.py index 419b3136..6d0d4261 100644 --- a/backend/app/models/punishment.py +++ b/backend/app/models/punishment.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import Optional +from app.models.punishment_type import PunishmentTypeRead from pydantic import BaseModel # pylint: disable=no-name-in-module from app.models.punishment_reaction import PunishmentReactionRead @@ -42,6 +43,10 @@ class PunishmentRead(PunishmentOut): user_id: UserId +class LeaderboardPunishmentRead(PunishmentRead): + punishment_type: PunishmentTypeRead + + class PunishmentStreaks(BaseModel): current_streak: int longest_streak: int diff --git a/backend/app/models/user.py b/backend/app/models/user.py index c470d862..1d6d1478 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -41,3 +41,10 @@ class LeaderboardPunishmentOut(PunishmentOut): class LeaderboardUser(User): punishments: list[LeaderboardPunishmentOut] total_value: int + + +class MinifiedLeaderboardUser(User): + total_value: int + emojis: str + amount_punishments: int + amount_unique_punishments: int diff --git a/frontend/package.json b/frontend/package.json index 4c81f98a..49de1849 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "react-router-dom": "^6.15.0", "react-simple-oauth2-login": "^0.5.4", "react-table": "^7.8.0", + "tailwind-scrollbar-hide": "^1.1.7", "zod": "^3.22.4" }, "devDependencies": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ad231807..f3b57731 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@headlessui/react': specifier: ^1.7.10 @@ -68,6 +64,9 @@ dependencies: react-table: specifier: ^7.8.0 version: 7.8.0(react@18.2.0) + tailwind-scrollbar-hide: + specifier: ^1.1.7 + version: 1.1.7 zod: specifier: ^3.22.4 version: 3.22.4 @@ -3042,6 +3041,10 @@ packages: tslib: 2.6.2 dev: false + /tailwind-scrollbar-hide@1.1.7: + resolution: {integrity: sha512-X324n9OtpTmOMqEgDUEA/RgLrNfBF/jwJdctaPZDzB3mppxJk7TLIDmOreEDm1Bq4R9LSPu4Epf8VSdovNU+iA==} + dev: false + /tailwindcss@3.2.4(postcss@8.4.31): resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} engines: {node: '>=12.13.0'} @@ -3297,3 +3300,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/src/components/leaderboardtable/LeaderboardTable.tsx b/frontend/src/components/leaderboardtable/LeaderboardTable.tsx index 84cfd637..bd863a8f 100644 --- a/frontend/src/components/leaderboardtable/LeaderboardTable.tsx +++ b/frontend/src/components/leaderboardtable/LeaderboardTable.tsx @@ -5,7 +5,7 @@ import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from "@tanst import { SkeletonTableItem } from "./SkeletonTableItem" import { LeaderboardTableItem } from "./LeaderboardTableItem" -import { useEffect, useRef } from "react" +import { useState } from "react" interface LeaderboardTableProps { leaderboardUsers?: LeaderboardUser[] | undefined @@ -13,38 +13,29 @@ interface LeaderboardTableProps { dataRefetch: ( options?: (RefetchOptions & RefetchQueryFilters) | undefined ) => Promise> - fetchNextPage: () => Promise> } -export const LeaderboardTable = ({ - leaderboardUsers, - isFetching, - dataRefetch, - fetchNextPage, -}: LeaderboardTableProps) => { - const ref = useRef(null) - - useEffect(() => { - document.addEventListener("scroll", () => { - if (ref.current && !isFetching) { - const lastItem = ref.current.firstElementChild?.lastElementChild - if (lastItem && lastItem.getBoundingClientRect().top < window.innerHeight) { - fetchNextPage().then() - } - } - }) - }, []) +export const LeaderboardTable = ({ leaderboardUsers, isFetching, dataRefetch }: LeaderboardTableProps) => { + const [currentlyOpen, setCurrentlyOpen] = useState(undefined) return ( -
    +
      setCurrentlyOpen(value)} className="divide-y divide-gray-100 dark:divide-gray-700 bg-white shadow-sm ring-1 ring-gray-900/5 rounded-lg md:rounded-xl" > {leaderboardUsers?.map((user, i) => ( - + ))} {leaderboardUsers && leaderboardUsers.length === 0 && !isFetching && ( diff --git a/frontend/src/components/leaderboardtable/LeaderboardTableItem.tsx b/frontend/src/components/leaderboardtable/LeaderboardTableItem.tsx index 385bda96..6a2982b0 100644 --- a/frontend/src/components/leaderboardtable/LeaderboardTableItem.tsx +++ b/frontend/src/components/leaderboardtable/LeaderboardTableItem.tsx @@ -1,51 +1,43 @@ -import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from "@tanstack/react-query" +import { QueryObserverResult, RefetchOptions, RefetchQueryFilters, useQuery } from "@tanstack/react-query" import { LeaderboardPunishment, LeaderboardUser } from "../../helpers/types" import { AccordionContent, AccordionItem, AccordionTrigger } from "../accordion/Accordion" import { textToEmoji } from "../../helpers/emojies" import { PunishmentList } from "../punishment/PunishmentList" -import { weeklyStreak } from "../../helpers/streaks" +import axios from "axios" +import { getLeaderboardUserPunishmentsUrl } from "../../helpers/api" interface TableItemProps { leaderboardUser: LeaderboardUser + isCurrentlyExpanded: boolean dataRefetch: ( options?: (RefetchOptions & RefetchQueryFilters) | undefined ) => Promise> i?: number } -export const LeaderboardTableItem = ({ leaderboardUser, dataRefetch, i }: TableItemProps) => { - const punishmentTypes = leaderboardUser.punishments.reduce((acc, punishment) => { - const oldValue = acc.get(punishment.punishment_type_id) - if (oldValue) { - acc.set(punishment.punishment_type_id, { - amount: oldValue.amount + punishment.amount, - punishment: punishment, - }) - } else { - acc.set(punishment.punishment_type_id, { amount: punishment.amount, punishment: punishment }) - } - return acc - }, new Map()) +export const LeaderboardTableItem = ({ leaderboardUser, isCurrentlyExpanded, dataRefetch, i }: TableItemProps) => { + const displayName = `${leaderboardUser.first_name} ${leaderboardUser.last_name}` + const { total_value: totalValue, emojis } = leaderboardUser - const totalPunishmentValue = leaderboardUser.punishments.reduce( - (acc, punishment) => acc + punishment.punishment_type.value * punishment.amount, - 0 + const { data: punishments, isLoading: isLoadingPunishments } = useQuery( + ["leaderboardUserPunishments", leaderboardUser.user_id], + () => axios.get(getLeaderboardUserPunishmentsUrl(leaderboardUser.user_id)).then((res) => res.data), + { enabled: isCurrentlyExpanded, staleTime: 1000 * 60 } ) - // Punishment dates to number from most recent to oldest - const dateToNumber = leaderboardUser?.punishments - .map((punishment) => { - const date = punishment.created_at.slice(0, 10) - const [year, month, day] = date.split("-").map(Number) - return new Date(year, month - 1, day).getTime() - }) - .reverse() - - const today = new Date().getTime() - const streak = weeklyStreak(today, dateToNumber) + const countOfEachEmojiInString = (str: string) => { + const counts: Record = {} + for (const char of str) { + counts[char] = counts[char] ? counts[char] + 1 : 1 + } + return counts + } - const displayName = leaderboardUser.first_name && leaderboardUser.last_name ? `${leaderboardUser.first_name} ${leaderboardUser.last_name}` : leaderboardUser.email + const emojisCounts = countOfEachEmojiInString(emojis) + const sortedEmojis = Object.entries(emojisCounts) + .map(([emoji, count]) => ({ emoji, count })) + .sort((a, b) => b.count - a.count) return ( @@ -69,55 +61,28 @@ export const LeaderboardTableItem = ({ leaderboardUser, dataRefetch, i }: TableI > {displayName}

      - {totalPunishmentValue}kr + {totalValue}kr
      -

      - {leaderboardUser.punishments.map((punishment) => - Array.from({ length: punishment.amount }, (_, i) => ( - - {punishment.punishment_type.emoji} - - )) - )} -

      -

      - {Object.entries(punishmentTypes) - .sort(([, a], [, b]) => b.punishment.punishment_type.value - a.punishment.punishment_type.value) - .map(([_, { amount, punishment }], i) => [ - - {amount}x - {punishment.punishmentType.emoji} - , - ])} +

      {emojis}

      +

      + {sortedEmojis.map(({ emoji, count }) => ( + + {emoji} + {count} + + ))}

      - {streak > 2 && ( -
      - - {streak} 🔥 - -
      - )} diff --git a/frontend/src/components/menu/Menu.tsx b/frontend/src/components/menu/Menu.tsx index 76237993..e6cfc685 100644 --- a/frontend/src/components/menu/Menu.tsx +++ b/frontend/src/components/menu/Menu.tsx @@ -11,7 +11,7 @@ export const Menu: FC = ({ icon, items }) => { return (
      - + {icon}
      diff --git a/frontend/src/components/punishment/PunishmentList.tsx b/frontend/src/components/punishment/PunishmentList.tsx index 36de29f3..f6aa3a98 100644 --- a/frontend/src/components/punishment/PunishmentList.tsx +++ b/frontend/src/components/punishment/PunishmentList.tsx @@ -4,6 +4,7 @@ import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from "@tanst import { Fragment } from "react" import { PunishmentActionBar } from "./PunishmentActionBar" import { PunishmentItem } from "./PunishmentItem" +import { SkeletonPunishmentItem } from "./SkeletonPunishmentItem" interface PunishmentListProps { groupUser?: GroupUser @@ -11,6 +12,7 @@ interface PunishmentListProps { groupId?: string punishments: Punishment[] punishmentTypes?: Record + isLoadingPunishments?: boolean dataRefetch: ( options?: (RefetchOptions & RefetchQueryFilters) | undefined ) => Promise> @@ -23,8 +25,13 @@ export const PunishmentList = ({ groupId, punishments, punishmentTypes, + isLoadingPunishments = false, dataRefetch, }: PunishmentListProps) => { + if (isLoadingPunishments) { + return + } + if (punishments.length === 0) { return {groupUser && } } diff --git a/frontend/src/components/punishment/SkeletonPunishmentItem.tsx b/frontend/src/components/punishment/SkeletonPunishmentItem.tsx new file mode 100644 index 00000000..39905e05 --- /dev/null +++ b/frontend/src/components/punishment/SkeletonPunishmentItem.tsx @@ -0,0 +1,9 @@ +export const SkeletonPunishmentItem = () => ( +
      +
      + + +
      + +
      +) diff --git a/frontend/src/helpers/api.ts b/frontend/src/helpers/api.ts index 95dd2486..0cc7f3db 100644 --- a/frontend/src/helpers/api.ts +++ b/frontend/src/helpers/api.ts @@ -35,6 +35,8 @@ const LEADERBOARD_URL = BASE_URL + "/users/leaderboard" const getLeaderboardUrl = (page: number) => `${LEADERBOARD_URL}?page=${page}` +export const getLeaderboardUserPunishmentsUrl = (userId: string) => `${LEADERBOARD_URL}/punishments/${userId}` + const GROUPS_URL = BASE_URL + "/groups/me" const ME_URL = BASE_URL + "/users/me" @@ -578,7 +580,7 @@ export const groupLeaderboardQuery = ( queryKey: ["groupLeaderboard", groupId], queryFn: () => axios.get(getGroupUrl(z.string().parse(groupId))).then((res) => GroupSchema.parse(res.data)), enabled: groupId !== undefined, - staleTime: 1000, + staleTime: 1000 * 60, }) export const publicGroupQuery = (groupNameShort?: string) => ({ @@ -588,7 +590,7 @@ export const publicGroupQuery = (groupNameShort?: string) => ({ .get(BASE_URL + `/groups/public_profiles/${z.string().parse(groupNameShort)}`) .then((res) => PublicGroupSchema.parse(res.data)), enabled: groupNameShort !== undefined, - staleTime: 1000, + staleTime: 1000 * 60, }) export const userQuery = () => { @@ -602,7 +604,7 @@ export const userQuery = () => { return MeUserSchema.parse(user) }, enabled: auth.isAuthenticated, - staleTime: 1000, + staleTime: 1000 * 60, } } @@ -612,7 +614,7 @@ export const committeesQuery = () => ({ axios.get(GROUP_STATISTICS_URL).then((res: AxiosResponse) => { return z.array(GroupStatisticsSchema).parse(Array.isArray(res.data) ? res.data : [res.data]) }), - staletime: 1000, + staletime: 1000 * 60, }) const getLeaderboard = async ({ pageParam = 0 }) => @@ -631,5 +633,5 @@ export const leaderboardQuery = (): UseInfiniteQueryOptions< const nextPage = lastPage.next ? new URL(lastPage.next).searchParams.get("page") : undefined return nextPage ? Number(nextPage) : undefined }, - staleTime: 1000, + staleTime: 1000 * 60, }) diff --git a/frontend/src/helpers/types.ts b/frontend/src/helpers/types.ts index fec6f3a0..a022eeeb 100644 --- a/frontend/src/helpers/types.ts +++ b/frontend/src/helpers/types.ts @@ -93,7 +93,9 @@ export type LeaderboardPunishment = z.infer export const LeaderboardUserSchema = UserSchema.extend({ total_value: z.number(), - punishments: z.array(LeaderboardPunishmentSchema), + emojis: z.string(), + amount_punishments: z.number(), + amount_unique_punishments: z.number(), }) export type LeaderboardUser = z.infer diff --git a/frontend/src/index.css b/frontend/src/index.css index f04d1a69..71f79c5b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -67,16 +67,16 @@ } } -/* For Webkit-based browsers (Chrome, Safari and Opera) */ -.scrollbar-hide::-webkit-scrollbar { +/* .scrollbar-hide::-webkit-scrollbar { display: none; } -/* For IE, Edge and Firefox */ .scrollbar-hide { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} + -ms-overflow-style: none; + scrollbar-width: none; +} */ + + .with-horizontal-scroll-shadow { background-position: left center, right center, left center, right center; @@ -84,3 +84,28 @@ background-size: 20px 100%, 20px 100%, 10px 100%, 10px 100%; background-attachment: local, local, scroll, scroll; } + +.lowkey-scrollbar { + width: 100%; + overflow-x: auto; + scrollbar-width: thin; +} + +.lowkey-scrollbar::-webkit-scrollbar { + display: block; + width: 0.4rem; + height: 0.4rem; +} + +.lowkey-scrollbar::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 5px; +} + +.lowkey-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: #666; +} + +.lowkey-scrollbar::-webkit-scrollbar-track { + background-color: transparent; +} diff --git a/frontend/src/pages/wallOfShame/WallOfShame.tsx b/frontend/src/pages/wallOfShame/WallOfShame.tsx index 63b400ba..0fb2af48 100644 --- a/frontend/src/pages/wallOfShame/WallOfShame.tsx +++ b/frontend/src/pages/wallOfShame/WallOfShame.tsx @@ -1,7 +1,7 @@ -import { useAuth } from "react-oidc-context" import { LeaderboardTable } from "../../components/leaderboardtable" import { useInfiniteQuery } from "@tanstack/react-query" import { leaderboardQuery } from "../../helpers/api" +import { Button } from "../../components/button/Button" export const WallOfShame = () => { const { isFetching, data, refetch, fetchNextPage } = useInfiniteQuery(leaderboardQuery()) @@ -15,8 +15,10 @@ export const WallOfShame = () => { leaderboardUsers={leaderboardUsers} isFetching={isFetching} dataRefetch={refetch} - fetchNextPage={fetchNextPage} /> + ) } diff --git a/frontend/src/views/groups/GroupView.tsx b/frontend/src/views/groups/GroupView.tsx index 9ca01165..0521088a 100644 --- a/frontend/src/views/groups/GroupView.tsx +++ b/frontend/src/views/groups/GroupView.tsx @@ -32,6 +32,7 @@ import { useGivePunishmentModal } from "../../helpers/context/modal/givePunishme import { GroupUserTable } from "../../components/groupusertable" import { useMutation, useQuery } from "@tanstack/react-query" import { signinAndReturn } from "../../helpers/auth" +import { AdditionalGroupNavItem } from "./tabnav/AdditionalGroupNavItem" export const GroupView = () => { const { currentUser, setCurrentUser } = useCurrentUser() @@ -194,44 +195,47 @@ export const GroupView = () => {
      {sidebarElement}
      -
      +
      group && navigate(`/gruppe/${group.name_short.toLowerCase()}`)} groups={user ? user.groups : undefined} /> - - {({ open }) => ( - <> - + + + {({ open }) => ( + <> + - - - - {sidebarElement} - - - - )} - + > + + + + {sidebarElement} + + + + )} + +
      {
      - + + } items={listItems} diff --git a/frontend/src/views/groups/tabnav/TabNav.tsx b/frontend/src/views/groups/tabnav/TabNav.tsx index 88b464a1..c47843c0 100644 --- a/frontend/src/views/groups/tabnav/TabNav.tsx +++ b/frontend/src/views/groups/tabnav/TabNav.tsx @@ -33,7 +33,7 @@ export const TabNav = ({ selectedGroup, setSelectedGroup, groups }: TabNavProps) <>
      @@ -48,7 +48,7 @@ export const TabNav = ({ selectedGroup, setSelectedGroup, groups }: TabNavProps) /> ))}
      - + {/* */} ) : ( diff --git a/frontend/src/views/groups/tabnav/TabNavItem.tsx b/frontend/src/views/groups/tabnav/TabNavItem.tsx index e7df355c..d87fa9a4 100644 --- a/frontend/src/views/groups/tabnav/TabNavItem.tsx +++ b/frontend/src/views/groups/tabnav/TabNavItem.tsx @@ -31,17 +31,18 @@ export const TabNavItem = ({ group, selectedGroup, ...props }: TabNavItemProps) aria-current={group.group_id ? "page" : undefined} {...props} > -