Track home improvements, fixes, repairs, and todos in one place.
- Create a Vite React TypeScript app.
mkdir HomeOps
cd HomeOps
npm create vite@latest . -- --template react-ts
npm install- Install app dependencies.
npm install @tremor/react @tanstack/react-query
npm install react-hook-form zod @hookform/resolvers
npm install clsx tailwind-merge- Install dev tooling.
npm install -D tailwindcss postcss autoprefixer
npm install -D eslint prettier
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
npm install -D typescript- Initialize Tailwind.
npx tailwindcss init -pIf you want shadcn/ui:
npx shadcn@latest initNotes:
- Pick Tailwind when prompted.
- Keep src/ as your base.
- You can add components later, for example:
npx shadcn@latest add button card input label
Create these folders and files.
HomeOps/
.github/
workflows/
ci.yml
security.yml
src/
components/
charts/
ui/
lib/
pages/
services/
types/
tests/
agents.md
amplify.yml
eslint.config.js
prettier.config.cjs
tailwind.config.ts
vite.config.ts
README.md
Replace tailwind.config.ts with this.
import type { Config } from 'tailwindcss'
export default {
content: [
'./index.html',
'./src/**/*.{ts,tsx}',
'./node_modules/@tremor/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
} satisfies ConfigReplace src/index.css with this.
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "@tremor/react/dist/esm/tremor.css";Create src/types/entry.ts.
export type EntryType = 'fix' | 'improvement' | 'repair' | 'todo'
export type HomeOpsEntry = {
id: string
created_at: string
type: EntryType
title: string
value?: number
notes?: string
}Create src/services/mockEntries.ts.
import { HomeOpsEntry } from '../types/entry'
export const mockEntries: HomeOpsEntry[] = [
{
id: '1',
created_at: '2025-01-01T09:00:00Z',
type: 'improvement',
title: 'Garage insulation',
value: 42,
notes: 'Added rigid foam',
},
{
id: '2',
created_at: '2025-01-02T09:00:00Z',
type: 'fix',
title: 'Door seal',
value: 45,
},
{
id: '3',
created_at: '2025-01-03T09:00:00Z',
type: 'todo',
title: 'Seal rim joists',
notes: 'Add to weekend list',
},
]Create src/services/entries.ts.
This is the only file you will swap later when you wire Supabase.
import { HomeOpsEntry } from '../types/entry'
import { mockEntries } from './mockEntries'
export async function listEntries(): Promise<HomeOpsEntry[]> {
return Promise.resolve(mockEntries)
}Create src/components/charts/EntriesLineChart.tsx.
import { Card, LineChart } from '@tremor/react'
import { HomeOpsEntry } from '../../types/entry'
type Props = {
data: HomeOpsEntry[]
}
export function EntriesLineChart({ data }: Props) {
const chartData = data
.filter(d => typeof d.value === 'number')
.map(d => ({
date: d.created_at.split('T')[0],
value: d.value as number,
}))
if (chartData.length === 0) {
return <Card>No data yet</Card>
}
return (
<Card>
<LineChart data={chartData} index="date" categories={['value']} yAxisWidth={40} />
</Card>
)
}Create src/pages/Dashboard.tsx.
import { useEffect, useState } from 'react'
import { EntriesLineChart } from '../components/charts/EntriesLineChart'
import { listEntries } from '../services/entries'
import { HomeOpsEntry } from '../types/entry'
export default function Dashboard() {
const [entries, setEntries] = useState<HomeOpsEntry[]>([])
useEffect(() => {
listEntries().then(setEntries)
}, [])
return (
<div className="p-6 space-y-6">
<h1 className="text-xl font-semibold">HomeOps Dashboard</h1>
<EntriesLineChart data={entries} />
</div>
)
}Replace src/App.tsx with this.
import Dashboard from './pages/Dashboard'
export default function App() {
return <Dashboard />
}Replace src/main.tsx with this.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)Replace vite.config.ts with this.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})Update package.json scripts to include these.
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier . --write",
"typecheck": "tsc -b --pretty false",
"test": "vitest run",
"test:watch": "vitest"
}
}Create eslint.config.js.
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'node_modules'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
},
)Install the ESLint peer deps used above.
npm install -D @eslint/js globals eslint-plugin-react-hooks eslint-plugin-react-refresh typescript-eslintCreate prettier.config.cjs.
module.exports = {
semi: false,
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
}Create src/test/setup.ts.
import '@testing-library/jest-dom'Update vite.config.ts to include test config.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
})Create a basic test at tests/smoke.test.tsx.
import { render, screen } from '@testing-library/react'
import App from '../src/App'
test('renders HomeOps dashboard title', () => {
render(<App />)
expect(screen.getByText('HomeOps Dashboard')).toBeInTheDocument()
})Create .github/workflows/ci.yml.
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm run test
- name: Build
run: npm run buildCreate .github/workflows/security.yml.
name: Security
on:
schedule:
- cron: '0 9 * * 1'
workflow_dispatch:
jobs:
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm audit --audit-level=highCreate amplify.yml.
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: dist
files:
- '**/*'
cache:
paths:
- node_modules/**/*Create agents.md and paste the locked rules for Supabase and Amplify hosting only.
Minimum additions for HomeOps branding.
- Repo name: HomeOps
- Purpose: track improvements, fixes, repairs, and todos
Create README.md.
# HomeOps
HomeOps is a simple system to track home improvements, fixes, repairs, and todos in one place.
## Local setup
1) Install deps
npm install
2) Run
npm run dev
## Scripts
- npm run lint
- npm run typecheck
- npm run test
- npm run build
## Deploy
This repo is deployed as a static app with AWS Amplify.npm run devLater, replace src/services/entries.ts with a Supabase backed implementation.
Do not change your UI components when you do that.