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
2 changes: 1 addition & 1 deletion apps/web/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"rsc": false,
"tsx": true,
"tailwind": {
"config": "../../packages/ui/tailwind.config.ts",
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true
Expand Down
36 changes: 18 additions & 18 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,46 @@
"messages:compile": "lingui compile --typescript",
"preview": "vite preview"
},
"packageManager": "bun@1.2.4",
"packageManager": "bun@1.2.6",
"dependencies": {
"@elysiajs/eden": "^1.2.0",
"@lingui/core": "^5.2.0",
"@lingui/react": "^5.2.0",
"@sinclair/typebox": "^0.34.28",
"@lingui/core": "^5.3.0",
"@lingui/react": "^5.3.0",
"@sinclair/typebox": "^0.34.31",
"@workspace/schema": "workspace:*",
"@workspace/ui": "workspace:*",
"browser-fs-access": "^0.35.0",
"clsx": "^2.1.1",
"idb-keyval": "^6.2.1",
"jotai": "^2.12.1",
"jotai-effect": "^2.0.1",
"jotai": "^2.12.2",
"jotai-effect": "^2.0.2",
"jotai-optics": "^0.4.0",
"lucide-react": "^0.479.0",
"lucide-react": "^0.484.0",
"optics-ts": "^2.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-hotkeys-hook": "^4.6.1"
},
"devDependencies": {
"@lingui/cli": "^5.2.0",
"@lingui/conf": "^5.2.0",
"@lingui/swc-plugin": "5.1.0",
"@lingui/vite-plugin": "^5.2.0",
"@tailwindcss/vite": "^4.0.6",
"@lingui/cli": "^5.3.0",
"@lingui/conf": "^5.3.0",
"@lingui/swc-plugin": "5.5.1",
"@lingui/vite-plugin": "^5.3.0",
"@tailwindcss/vite": "^4.0.16",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@tsconfig/vite-react": "^3.4.0",
"@types/bun": "^1.2.4",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/bun": "^1.2.6",
"@types/node": "^22.13.13",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react-swc": "3.8.0",
"@vitejs/plugin-react-swc": "3.8.1",
"@workspace/eslint-config": "workspace:*",
"elysia": "^1.2.25",
"happy-dom": "^17.4.2",
"tailwindcss": "^4.0.6",
"happy-dom": "^17.4.4",
"tailwindcss": "^4.0.16",
"vite-tsconfig-paths": "^5.1.4"
}
}
1 change: 0 additions & 1 deletion apps/web/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function App() {
</Suspense>
</main>

<footer className="pb-3 lg:pb-7">
<footer className="relative pb-3 lg:pb-7">
<section className="mx-auto grid max-w-4xl grid-cols-1 gap-12 lg:grid-cols-3">
<Waveform />
</section>
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/utils/atomWithExpire.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { atom } from 'jotai';
import { RESET, atomWithReset } from 'jotai/utils';

/**
* Creates a Jotai atom that expires after a specified time.
*
* This function creates an atom that holds a value and automatically resets
* to its initial value after the given timeout. The timeout can be provided
* with each write operation. If no timeout is provided, the atom behaves like
* a regular `atomWithReset`.
*
* @param initialValue - The initial value of the atom.
*/
export function atomWithExpire<T>(initialValue: T) {
const timeoutAtom = atom<ReturnType<typeof setTimeout> | undefined>(
undefined
Expand Down
39 changes: 39 additions & 0 deletions apps/web/src/utils/atomWithExpiringWriteState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,49 @@ import { atomWithExpire } from './atomWithExpire';
import { LoadableState } from '@/types';

export interface AtomWithExpiringWriteStateOptions {
/**
* Time in milliseconds before the state expires. Defaults to `DEFAULT_LOADABLE_STATE_TIMEOUT`.
*/
expireInMs?: number;

/**
* Determines if an `error` should be ignored.
*
* If `true` is returned, the error state will be set to `null` instead of `LOADABLE_STATE.HAS_ERROR`.
*/
shouldIgnoreError?: (error: unknown) => boolean;
}

/**
* Creates a Jotai atom that handles async write operations with an expiring state.
*
* This function wraps an asynchronous write operation with a Jotai atom that tracks the loadable state
* (`LOADING`, `HAS_DATA`, `HAS_ERROR`, or `null`). The state expires after a specified timeout.
*
* Use this to provide users with immediate, short-lived feedback, like success or error elements,
* after they perform an action.
*
* @example
* ```
* const fetchDataAtom = atomWithExpiringWriteState(async (get, set, url: string) => {
* const response = await fetch(url);
* return response.json();
* });
*
* // Usage
* const [state, fetchData] = useAtom(fetchDataAtom);
* if (state === LOADABLE_STATE.LOADING) {
* // Show loading indicator
* } else if (state === LOADABLE_STATE.HAS_ERROR) {
* // Show error
* } else {
* // Show data
* }
* ```
*
* @param write - The asynchronous write function that performs the operation.
* @param options - Options for configuring the atom's behavior.
*/
export function atomWithExpiringWriteState<Args extends unknown[], Result>(
write: (get: Getter, set: Setter, ...args: Args) => Result,
options: AtomWithExpiringWriteStateOptions = {}
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/utils/atomWithToggleAndStorage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { WritableAtom, atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

/**
* Creates a Jotai atom with storage and toggle functionality.
*
* This function combines `atomWithStorage` for persistent storage with a custom
* writable atom that toggles the stored boolean value when updated without a
* specific value.
*
* @param args - Arguments passed to to `atomWithStorage`.
*/
export function atomWithToggleAndStorage(
...args: Parameters<typeof atomWithStorage<boolean>>
): WritableAtom<boolean, [boolean?], void> {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import type { App } from '../../../api/src/index';

export const client = treaty<App>(import.meta.env.VITE_API_URL);

/**
* Type guard function to check if an `error` is a validation error.
*
* @param error - The error object to check.
*/
export function isValidationError(
error: unknown
): error is { path?: string; message?: string } {
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/utils/createAbortablePromise.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
type Executor<T> = ConstructorParameters<typeof Promise<T>>[0];

export interface CreateAbortablePromiseOptions {
/**
* An `AbortSignal` that can be used to abort the promise.
*/
signal?: AbortSignal;
}

/**
* Creates a promise that can be aborted using an `AbortSignal`.
*
* @param executor - The executor function (same as for a standard `Promise` constructor)/
* @param param1 - Optional parameters.
*/
export function createAbortablePromise<T>(
executor: Executor<T>,
{ signal }: CreateAbortablePromiseOptions = {}
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/utils/createGraphIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { createLucideIcon } from 'lucide-react';
const width = 24;
const height = 24;

/**
* Creates a Lucide icon representing a graph based on the provided function.
*
* The function `getY` should map an x-coordinate to a y-coordinate within the icon's bounds.
*
* @param iconName - The name of the Lucide icon to create.
* @param getY - A function that takes an x-coordinate (number) and returns a y-coordinate (number) for the graph.
*/
export function createGraphIcon(iconName: string, getY: (x: number) => number) {
const points: number[] = [];
for (let x = 0; x <= width; x++) {
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/utils/loadImage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { createAbortablePromise } from '@/utils/createAbortablePromise';

export interface LoadImageOptions {
/**
* Optional `AbortSignal` to allow cancellation of the image loading.
*/
signal?: AbortSignal;
}

/**
* Loads an image from given source URL.
*
* It creates an `HTMLImageElement` and sets its `src` attribute. It returns a promise
* that resolves with the loaded image element when the image has successfully loaded,
* or rejects with an error if the image fails to load or if the loading is aborted.
*
* @param src - The URL of the image to load.
* @param options - Optional parameters.
*/
export function loadImage(
src: string,
options?: LoadImageOptions
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/utils/rasterize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ import { MIME_TYPES } from '@/constants';
import { loadImage } from '@/utils/loadImage';

export interface RasterizeOptions {
/**
* The MIME type for the rasterized image (e.g., `image/png`).
*/
type?: string;
}

/**
* Rasterizes an SVG element into an image (blob).
*
* @param svg - The SVG element to rasterize.
* @param options - Rasterization options.
*
* @returns A promise that resolves with a Blob containing the rasterized image, or rejects with an error if any.
*/
export async function rasterize(
svg: SVGGraphicsElement,
options: RasterizeOptions = {}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/utils/readFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createAbortablePromise } from '@/utils/createAbortablePromise';

export interface ReadFileOptions {
/**
* Optional AbortSignal to allow cancellation of the file reading.
* Optional `AbortSignal` to allow cancellation of the file reading.
*/
signal?: AbortSignal;
}
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/utils/waveFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/**
* {@inheritdoc Math.sin}
*/
export const sin = Math.sin;

/**
* Calculates a "heartbeat" wave function.
*
* This function combines three sine waves with slightly different frequencies
* and averages them, creating a more complex oscillating pattern.
*
* @param x - The input value measured in radians.
*/
export const heartbeat = (x: number) =>
(Math.sin(x) + Math.sin(x * 1.1) + Math.sin(x * 1.9)) / 3;
Loading