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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The codebase uses Docker to run third-party services locally (Meilisearch for se
- Use `== null` (double equals null) instead of either `=== null` or `=== undefined` (triple equals null / undefined), and the same for `!=`. This is to make `null` and `undefined` mean the same thing everywhere in our codebase to avoid any potential serialisation/deserialisation confusion or issues.
- Avoid use of `any` unless absolutely necessary.
- Prefer early-exit if statements rather than nested if statements.
- Avoid the use of barrel files

# CSS

Expand Down
4 changes: 3 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiProvider } from 'app/api/api_provider';
import { MaintenanceBanner } from 'app/maintenance_banner';
import { SkeletonProvider } from 'app/skeleton_provider';
import type { Metadata } from 'next';
import { getFlags } from 'services/server_context';
import { SessionProvider } from 'session/session_provider';
Expand All @@ -8,7 +9,7 @@ import { ThemeProvider } from 'ui/base/theme';
import { NavBar } from 'ui/nav_bar/nav_bar';
import './globals.css';
import styles from './layout.module.css';
import { SkeletonProvider } from 'app/skeleton_provider';
import { ToastProvider } from 'ui/base/toast/toast';

export const metadata: Metadata = {
title: 'ParaDB',
Expand All @@ -35,6 +36,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
) : null}
<NavBar />
<div className={styles.content}>{children}</div>
<ToastProvider />
</SkeletonProvider>
</SessionProvider>
</ApiProvider>
Expand Down
1 change: 1 addition & 0 deletions src/app/map_list_presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { makeAutoObservable, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
import { MapSortableAttributes, PDMap, mapSortableAttributes } from 'schema/maps';
import { TableSortStore } from 'ui/base/table/table_presenter';
import { ToastIntent, showToast } from 'ui/base/toast/toast';
import { getMapFileLink } from 'utils/maps';

const SEARCH_LIMIT = 20;
Expand Down
5 changes: 2 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useApi } from 'app/api/api_provider';
import { MapListPresenter, MapListStore } from 'app/map_list_presenter';
import { useSkeletonRef } from 'app/skeleton_provider';
import classNames from 'classnames';
import useInfiniteScroll from 'hooks/useInfiniteScroll';
import { action, computed, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { useSearchParams } from 'next/navigation';
Expand All @@ -18,9 +20,6 @@ import { KnownDifficulty, difficultyColors, parseDifficulty } from 'utils/diffic
import { RoutePath, routeFor } from 'utils/routes';
import styles from './page.module.css';
import { Search } from './search';
import useInfiniteScroll from 'hooks/useInfiniteScroll';
import { useSkeletonRef } from 'app/skeleton_provider';
import { Tooltip } from 'ui/base/tooltip/tooltip';

export default function Page() {
return (
Expand Down
1 change: 1 addition & 0 deletions src/ui/base/design_system/design_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const colors = {
green: '#4c4',
red: '#f00',
purple: '#9b15f1',
accent: '#9b15f1',
} as const;

export const metrics = {
Expand Down
64 changes: 64 additions & 0 deletions src/ui/base/toast/toast.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.toastRegion {
position: fixed;
right: calc(var(--gridBaseline) * 5);
bottom: calc(var(--gridBaseline) * 5);
z-index: 1000;

display: flex;
flex-direction: column;
gap: var(--gridBaseline);
}

.toast {
display: flex;
align-items: center;
gap: var(--gridBaseline);

padding: calc(var(--gridBaseline) * 1.5) calc(var(--gridBaseline) * 2);
background: var(--colorBackground);
border: 1px solid var(--colorAccent);

animation: slideIn 0.2s ease-out;
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.content {
flex: 1;
}

.closeButton {
display: flex;
align-items: center;
justify-content: center;
margin-left: var(--gridBaseline);

background: transparent;
border: none;
color: var(--colorForeground);
cursor: pointer;
opacity: 0.6;
transition: opacity 0.15s ease;
}

.closeButton:hover {
opacity: 1;
}

/* Intent variants */
.success {
border-color: var(--colorGreen);
}

.error {
border-color: var(--colorRed);
}
69 changes: 69 additions & 0 deletions src/ui/base/toast/toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client';

import { ToastQueue } from '@react-stately/toast';
import classNames from 'classnames';
import {
Button,
UNSTABLE_Toast as Toast,
UNSTABLE_ToastContent as ToastContent,
UNSTABLE_ToastRegion as ToastRegion,
} from 'react-aria-components';
import { T } from 'ui/base/text/text';
import styles from './toast.module.css';

const intentStyles: Record<ToastIntent, string> = {
[ToastIntent.DEFAULT]: styles.default,
[ToastIntent.ERROR]: styles.error,
[ToastIntent.SUCCESS]: styles.success,
};

export const enum ToastIntent {
DEFAULT = 'default',
SUCCESS = 'success',
ERROR = 'error',
}

export type ToastData = {
message: string;
intent?: ToastIntent;
};

const toastQueue = new ToastQueue<ToastData>({ maxVisibleToasts: 5 });

/**
* Shows a toast notification.
* @param message The message to display
* @param intent The visual intent: 'default' (purple border), 'success' (green), or 'error' (red)
* @param timeout Duration in ms before auto-dismiss (default: 5000)
*/
export function showToast(
message: string,
intent: ToastIntent = ToastIntent.DEFAULT,
timeout: number = 5000
): void {
toastQueue.add({ message, intent }, { timeout });
}

export function ToastProvider() {
return (
<ToastRegion queue={toastQueue} className={styles.toastRegion}>
{({ toast }) => {
const intent = toast.content.intent ?? 'default';
return (
<Toast toast={toast} className={classNames(styles.toast, intentStyles[intent])}>
<ToastContent className={styles.content}>
<T.Medium>{toast.content.message}</T.Medium>
</ToastContent>
<Button
className={styles.closeButton}
onPress={() => toastQueue.close(toast.key)}
slot="close"
>
</Button>
</Toast>
);
}}
</ToastRegion>
);
}
2 changes: 1 addition & 1 deletion src/ui/base/tooltip/tooltip.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
color: var(--colorForeground);
padding: var(--gridBaseline) calc(var(--gridBaseline) * 1.5);
border-radius: calc(var(--gridBaseline) * 0.5);
border: 1px solid var(--colorPurple);
border: 1px solid var(--colorAccent);
box-sizing: border-box;
font-size: 14px;
max-width: calc(var(--gridBaseline) * 32);
Expand Down
2 changes: 1 addition & 1 deletion src/ui/base/tooltip/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const Tooltip = (props: TooltipProps) => {
<AriaTooltip placement={placement} className={styles.tooltip}>
<OverlayArrow className={styles.arrow}>
<svg width={8} height={8} viewBox="0 0 8 8">
<path strokeWidth="1" stroke={colors.purple} d="M0 0 L4 4 L8 0" />
<path strokeWidth="1" stroke={colors.accent} d="M0 0 L4 4 L8 0" />
</svg>
</OverlayArrow>
{content}
Expand Down