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
40 changes: 40 additions & 0 deletions .github/workflows/storybook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Build Storybook

on:
pull_request:
push:
branches: [ main ]

jobs:
build-storybook:
runs-on: ubuntu-latest
defaults:
run:
working-directory: client
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: client/package-lock.json

- name: Install dependencies
run: npm ci

- name: Lint (optional)
run: npm run lint --if-present

- name: Build Storybook
run: npm run build-storybook

# Optional: Upload the static build as an artifact for preview
- name: Upload storybook-static artifact
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: client/storybook-static
if-no-files-found: ignore
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# expense-tracker
<div align="center">
<h1 style="margin-bottom: 0.25rem;">Expense Tracker</h1>
<p style="margin-top: 0; color: #6b7280;">Monorepo containing a React (Vite) client and an Express server.</p>
<p>
<img alt="React" src="https://img.shields.io/badge/Client-React%20%2B%20Vite-61dafb?logo=react&logoColor=white" />
<img alt="Express" src="https://img.shields.io/badge/Server-Express-000000?logo=express&logoColor=white" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript&logoColor=white" />
</p>
</div>

<h2>Structure</h2>

```
expense-tracker/
client/ # React + TS + Vite app (UI library + Storybook)
server/ # Express API server
```

<h2>Tech Stack</h2>

- Client: React 18, TypeScript, Vite, Tailwind CSS, Storybook
- Server: Node.js, Express, PostgreSQL (via pg), Helmet, CORS, Morgan

<h2>Quick Start</h2>

1) Server

```bash
cd server
cp .env.example .env # edit values as needed
npm install
npm run dev
```

By default: http://localhost:8080 (configure CORS_ORIGIN to your client URL)

2) Client

```bash
cd client
npm install
npm run dev
```

Vite dev server runs on http://localhost:5173 by default.

3) Storybook (optional)

```bash
cd client
npm run storybook
```

<h2>Documentation</h2>

- Client README: ./client/README.md
- Server README: ./server/README.md

<h2>Environment</h2>

- Server env template: `server/.env.example`
- Key vars: `PORT`, `DATABASE_URL`, `JWT_SECRET`, `CORS_ORIGIN`

<!-- Project-specific contribution guidelines and CI/CD details live outside this file. -->
4 changes: 3 additions & 1 deletion client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ dist-ssr
*.sln
*.sw?

.env
.env
*storybook.log
storybook-static
20 changes: 20 additions & 0 deletions client/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
"@chromatic-com/storybook",
"@storybook/addon-docs",
"@storybook/addon-onboarding",
"@storybook/addon-a11y",
"@storybook/addon-vitest"
],
"framework": {
"name": "@storybook/react-vite",
"options": {}
}
};
export default config;
24 changes: 24 additions & 0 deletions client/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Preview } from '@storybook/react-vite'
import '../src/index.css';
import { ToastProvider } from '../src/ui';
import React from 'react';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(Story) => React.createElement(
ToastProvider,
null,
React.createElement(Story, null)
),
],
};

export default preview;
143 changes: 76 additions & 67 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,78 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...

// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,

// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
<div align="center">
<h1 style="margin-bottom: 0.25rem;">Expense Tracker Client</h1>
<p style="margin-top: 0; color: #6b7280;">Modern React + TypeScript + Vite frontend with a reusable UI library and Storybook docs.</p>
<p>
<img alt="React" src="https://img.shields.io/badge/React-18-61dafb?logo=react&logoColor=white" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript&logoColor=white" />
<img alt="Vite" src="https://img.shields.io/badge/Vite-4-646cff?logo=vite&logoColor=white" />
<img alt="Tailwind" src="https://img.shields.io/badge/TailwindCSS-4-06b6d4?logo=tailwindcss&logoColor=white" />
<img alt="Storybook" src="https://img.shields.io/badge/Storybook-8-ff4785?logo=storybook&logoColor=white" />
</p>
</div>

## Tech Stack

- React 18 + TypeScript
- Vite
- Tailwind CSS
- Storybook

## Requirements

- Node.js >= 18
- pnpm, npm, or yarn (examples below use npm)

<h2>Scripts</h2>

- dev server: `npm run dev`
- build: `npm run build`
- preview: `npm run preview`
- storybook: `npm run storybook`

<h2>UI Components Overview (<code>client/src/ui/</code>)</h2>

- Tooltip (`Tooltip.tsx`)
- Uses shared positioning hook `useAnchoredPopover` for smart placement (top/bottom/left/right), flip and clamp to viewport.
- A11y: `role="tooltip"`, dynamic `aria-describedby`, subtle motion to reduce distraction.
- Docs: see `Tooltip.stories.tsx` for placements, delays, disabled listeners, custom classes.

- Dialog (`Dialog.tsx`)
- Modal centered with backdrop, ESC to close, optional overlay click to close, scroll lock.
- Content is flexible: supports cards, long lists (use an inner `max-h-[60vh] overflow-auto`).
- Docs: see `Dialog.stories.tsx` including cards grid and long list examples (copy-paste code provided in docs panel).

- DatePicker, Select, Skeleton, TextField, Button, Toast, IconButton
- See respective `*.stories.tsx` for usage.

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
<h3>Shared hooks and constants</h3>
- `hooks/useAnchoredPopover.ts`: shared anchor-based popover positioning for tooltips/menus/popovers.
- `constants/tooltip.ts` and other files in `constants/`: centralized Tailwind classes and UI tokens.

<h2>Folder Structure (excerpt)</h2>

```
client/
src/ui/
Tooltip.tsx
Dialog.tsx
hooks/useAnchoredPopover.ts
constants/tooltip.ts
*.stories.tsx
```

<h2>Accessibility</h2>

- Components aim for sensible roles/aria attributes and keyboard support (e.g., ESC for dialogs).
- Prefer linking labels with `aria-labelledby` / `aria-describedby` where relevant.

<h2>Design & Styling</h2>

- Styling via Tailwind utility classes. Shared tokens live under `src/ui/constants/`.

<h2>Storybook</h2>

- Run `npm run storybook` and open the browser UI.
- Each story exposes a “Show code”/Docs panel with copy‑paste snippets for quick adoption.

<!-- Internal workflows and branching strategies are managed at the repository level. -->

20 changes: 18 additions & 2 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";

import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
Expand All @@ -6,7 +9,7 @@ import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
globalIgnores(['dist']),
globalIgnores(['dist', 'storybook-static']),
{
files: ['**/*.{ts,tsx}'],
extends: [
Expand All @@ -20,4 +23,17 @@ export default tseslint.config([
globals: globals.browser,
},
},
])
{
files: ['src/constants/*.tsx', 'src/ui/Toast.tsx'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/ui/Tooltip.tsx'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'react-hooks/exhaustive-deps': 'warn',
},
},
], storybook.configs["flat/recommended"]);
Loading