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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules
.env
.vscode
.mcp.json

*.tsbuildinfo
.tsbuildinfo
Expand Down
Binary file added .playwright-mcp/page-2026-01-04T16-08-22-788Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-10-49-313Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-11-14-268Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-31-22-308Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-36-23-230Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-37-45-837Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-52-23-837Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-53-01-868Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-55-04-138Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T16-56-05-826Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T17-01-22-690Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T17-02-15-302Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T18-10-29-487Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T18-11-10-223Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T18-14-52-888Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T18-15-13-460Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .playwright-mcp/page-2026-01-04T18-17-28-398Z.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions packages/api/src/services/auth/devLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import crypto from 'crypto';
import { db } from 'db';
import createDebug from 'debug';

const debug = createDebug('backend-ts:auth');

function generateSessionId(): string {
return crypto.randomBytes(16).toString('hex');
}

export async function devLogin(playerId: number): Promise<{ session: string }> {
debug(`Dev login for player ID: ${playerId}`);

// Verify player exists
const player = await db
.selectFrom('players')
.select(['id', 'nickname'])
.where('id', '=', playerId)
.executeTakeFirst();

if (!player) {
throw new Error(`Player with ID ${playerId} not found`);
}

debug(`Found player: ${player.nickname} (${player.id})`);

// Delete old sessions for this player
await db.deleteFrom('sessions').where('player', '=', player.id).execute();

// Create new session (valid for 2 weeks)
const now = new Date();
const sessionId = generateSessionId();
const validUntil = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);

await db
.insertInto('sessions')
.values({
id: sessionId,
player: player.id,
established: now,
valid_until: validUntil,
})
.execute();

debug(`Created dev session for player ${player.id}`);

return { session: sessionId };
}
14 changes: 13 additions & 1 deletion packages/api/src/trpc/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { devLogin as devLoginService } from 'services/auth/devLogin';
import { getRegistrationToken as getRegistrationTokenService } from 'services/auth/getRegistrationToken';
import { loginWithGoogleCredential } from 'services/auth/googleLogin';
import { logout as logoutService } from 'services/auth/logout';
import { registerPlayer } from 'services/auth/register';
import { publicProcedure, router } from 'trpc/trpc';
import { devProcedure, publicProcedure, router } from 'trpc/trpc';
import { z } from 'zod';

export const loginGoogle = publicProcedure
Expand Down Expand Up @@ -44,9 +45,20 @@ export const logout = publicProcedure.mutation(async ({ ctx }) => {
}
});

export const devLogin = devProcedure
.input(
z.object({
playerId: z.number(),
})
)
.mutation(({ input }) => {
return devLoginService(input.playerId);
});

export const auth = router({
loginGoogle,
getRegistrationToken,
register,
logout,
devLogin,
});
10 changes: 10 additions & 0 deletions packages/api/src/trpc/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,13 @@ export const adminProcedure = t.procedure.use(async ({ ctx, next }) => {
}
return next({ ctx: { user: ctx.user } });
});

/**
* Dev procedure - only available in development mode
*/
export const devProcedure = t.procedure.use(async ({ next }) => {
if (process.env.NODE_ENV !== 'development') {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Only available in development mode' });
}
return next();
});
14 changes: 14 additions & 0 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ import { queryClient } from 'utils/trpc';
const theme = createTheme({
autoContrast: true,
primaryColor: 'gray',
breakpoints: {
xs: '30em',
sm: '48em',
md: '64em',
lg: '74em',
xl: '90em',
},
lineHeights: {
xs: '1.2',
sm: '1.3',
md: '1.4',
lg: '1.5',
xl: '1.6',
},
spacing: {
xxs: '0.25rem',
xs: '0.625rem',
Expand Down
5 changes: 3 additions & 2 deletions packages/web/src/components/Flag/Flag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import './flag.scss';
interface FlagProps {
region: string;
className?: string;
size?: 'sm' | 'md';
}

export const Flag = memo(function Flag({ region, className }: FlagProps) {
export const Flag = memo(function Flag({ region, className, size = 'md' }: FlagProps) {
return (
<img
className={`flag ${className ?? ''}`}
className={`flag flag--${size} ${className ?? ''}`}
src={`https://osu.ppy.sh/images/flags/${region}.png`}
alt={`${region} flag`}
/>
Expand Down
13 changes: 11 additions & 2 deletions packages/web/src/components/Flag/flag.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
.flag {
width: 20px;
height: 15px;
border-radius: 3px;
opacity: 0.85;

&--md {
width: 20px;
height: 15px;
}

&--sm {
width: 16px;
height: 12px;
border-radius: 2px;
}
}
4 changes: 2 additions & 2 deletions packages/web/src/components/TopBar/top-bar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

@media screen and (max-width: 500px) {
.top-bar > nav > ul {
font-size: 4.4vw;
font-size: 16px;
> li > a {
margin: 1vw;
margin: 4px;
}
}
}
24 changes: 8 additions & 16 deletions packages/web/src/features/leaderboards/components/charts/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionIcon, Text } from '@mantine/core';
import { ActionIcon, Group, Text } from '@mantine/core';
import { useAtomValue } from 'jotai';
import _ from 'lodash/fp';
import { Fragment, memo, useState } from 'react';
Expand All @@ -9,8 +9,6 @@ import { useHighlightedPlayerIds } from 'features/leaderboards/hooks/useHighligh

import { useUser } from 'hooks/useUser';

import { useLanguage } from 'utils/context/translation';

import type { ChartApiOutput } from '../../hooks/useChartsQuery';
import { ChartHeader } from './ChartHeader/ChartHeader';
import Result from './Result';
Expand All @@ -21,7 +19,6 @@ const Chart = memo(function _Chart({ chart }: { chart: ChartApiOutput }) {
const filter = useAtomValue(filterAtom);
const highlightedPlayerIds = useHighlightedPlayerIds();
const [showHidden, setShowHidden] = useState(false);
const lang = useLanguage();

const hiddenResultsCount = chart.results.reduce((sum, res) => (res.isHidden ? sum + 1 : sum), 0);

Expand Down Expand Up @@ -96,18 +93,13 @@ const Chart = memo(function _Chart({ chart }: { chart: ChartApiOutput }) {
<div className="song-block">
<ChartHeader chart={chart}>
{hiddenResultsCount > 0 && (
<>
<Text size="xs" lh={1}>
{lang.HIDDEN}: {hiddenResultsCount}
</Text>
<ActionIcon
variant="subtle"
aria-label="Show all"
onClick={() => setShowHidden(!showHidden)}
>
<FaGlobeAmericas />
</ActionIcon>
</>
<ActionIcon
variant="subtle"
aria-label="Show all"
onClick={() => setShowHidden(!showHidden)}
>
<FaGlobeAmericas />
</ActionIcon>
)}
</ChartHeader>
<div className="charts">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import qs from 'query-string';
import { FaYoutube } from 'react-icons/fa';
import { NavLink } from 'react-router-dom';

import css from './chart-header.module.scss';

import { ChartLabel } from 'components/ChartLabel/ChartLabel';

import { colorByMix } from 'constants/colors';
Expand All @@ -26,45 +24,43 @@ export const ChartHeader = ({ chart, children = null }: ChartHeaderProps): JSX.E
const [chartType, chartLevel] = labelToTypeLevel(chart.label);

return (
<div className={css.songHeader}>
<Group p="xs" bdrs="xl" gap="sm" align="center">
<ChartLabel type={chartType} level={chartLevel ?? '?'} />
<div className={css.songName}>
<Anchor
size="xl"
fw="bold"
component={NavLink}
to={routes.leaderboard.sharedChart.getPath({ sharedChartId: chart.id })}
>
{chart.songName}
</Anchor>{' '}
<Text component="span" c="dimmed">
{chart.difficulty ? `(${chart.difficulty.toFixed(1)}) ` : ''}
</Text>
</div>
<div className={css.youtubeLink}>
<a
href={`https://youtube.com/results?${qs.stringify({
search_query: `${chart.songName} ${chart.label}`.replace(/( -)|(- )/g, ' '),
})}`}
target="_blank"
rel="noopener noreferrer"
>
<FaYoutube />
</a>
</div>
<Group gap="xs" ml="auto" fz="sm">
{otherInstances.map((instance) => {
if (instance.level === chart.level) {
return null;
}
return (
<Badge key={instance.mix} color={colorByMix[instance.mix as keyof typeof colorByMix]}>
{Mixes[instance.mix as keyof typeof Mixes]}: {instance.label}
</Badge>
);
})}
{children}
</Group>
</div>
<Anchor
flex="1 1 0"
size="xl"
lh="xs"
fw="bold"
component={NavLink}
to={routes.leaderboard.sharedChart.getPath({ sharedChartId: chart.id })}
>
{chart.songName}
</Anchor>{' '}
<Text component="span" c="dimmed">
{chart.difficulty ? `(${chart.difficulty.toFixed(1)}) ` : ''}
</Text>
<Anchor
href={`https://youtube.com/results?${qs.stringify({
search_query: `${chart.songName} ${chart.label}`.replace(/( -)|(- )/g, ' '),
})}`}
target="_blank"
rel="noopener noreferrer"
fz="xl"
lh={1}
>
<FaYoutube />
</Anchor>
{otherInstances.map((instance) => {
if (instance.level === chart.level) {
return null;
}
return (
<Badge key={instance.mix} color={colorByMix[instance.mix as keyof typeof colorByMix]}>
{Mixes[instance.mix as keyof typeof Mixes]}: {instance.label}
</Badge>
);
})}
{children}
</Group>
);
};

This file was deleted.

Loading