Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
46b7995
clean up compose file env vars
aditya-arcot Mar 14, 2026
90f24ab
add actrc, update readme
aditya-arcot Mar 14, 2026
011ac68
client - add custom view label support for data table
aditya-arcot Mar 14, 2026
d1cfc46
client - add initial exercises page
aditya-arcot Mar 14, 2026
8a5dccf
client - add filter only column support for data table
aditya-arcot Mar 14, 2026
d990bbc
client - display system exercise using icon instead of separate type …
aditya-arcot Mar 14, 2026
e6ce41c
client - update deps
aditya-arcot Mar 14, 2026
2fec670
client - add truncated cells with tooltip to data table
aditya-arcot Mar 14, 2026
721502c
client - add truncation to exercise table columns
aditya-arcot Mar 14, 2026
9bbd774
client - add muscle groups filter to exercises table
aditya-arcot Mar 14, 2026
864be10
client - remove labels from action items
aditya-arcot Mar 14, 2026
2b9709f
client - standardize button styles
aditya-arcot Mar 14, 2026
f5c1d20
client - use override for custom button
aditya-arcot Mar 14, 2026
6453741
client - update components
aditya-arcot Mar 14, 2026
e6fa9a2
client - remove margin from action icons
aditya-arcot Mar 14, 2026
a87ad6e
client - standardize loading state variable names
aditya-arcot Mar 14, 2026
5ae6deb
client - fix search input value clear on reset in data table toolbar
aditya-arcot Mar 14, 2026
7859ce9
client - remove unused select column
aditya-arcot Mar 14, 2026
5be0116
client - fix column padding
aditya-arcot Mar 15, 2026
6c36e7e
Bump lint-staged from 16.3.3 to 16.4.0 in the root-dependencies group
dependabot[bot] Mar 16, 2026
b71efeb
Bump @vitejs/plugin-react in /client in the client-dependencies group
dependabot[bot] Mar 16, 2026
d65262e
Bump the server-dependencies group in /server with 2 updates
dependabot[bot] Mar 16, 2026
aadbc21
Merge pull request #93 from arcot-labs/dependabot/npm_and_yarn/stage/…
aditya-arcot Mar 16, 2026
9a47dae
Merge pull request #95 from arcot-labs/dependabot/uv/server/stage/ser…
aditya-arcot Mar 16, 2026
d66b318
Merge pull request #94 from arcot-labs/dependabot/npm_and_yarn/client…
aditya-arcot Mar 16, 2026
41cce72
chore: format & lint after PR merge
github-actions[bot] Mar 16, 2026
57ed998
server - fix deletion of exercises with muscle groups
aditya-arcot Mar 16, 2026
b981c11
Merge branch 'stage' into dev
aditya-arcot Mar 16, 2026
35e6bb1
client - implement exercise deletion
aditya-arcot Mar 16, 2026
21f61f0
server - sort muscle groups in exercise object
aditya-arcot Mar 16, 2026
ff81818
server - fix tests broken by exercise seeding
aditya-arcot Mar 16, 2026
c81f3ba
server - add downgrade command to makefile
aditya-arcot Mar 16, 2026
8e65ed3
client - rename feedback component
aditya-arcot Mar 16, 2026
5723068
client - refactor dialog to use footer
aditya-arcot Mar 16, 2026
f5520e9
client - add labels to title & description fields
aditya-arcot Mar 16, 2026
27b58c1
client - finish exercises page
aditya-arcot Mar 16, 2026
3da7643
client - update button variant
aditya-arcot Mar 16, 2026
f0e2538
add muscle groups reseed migration
aditya-arcot Mar 16, 2026
efced1f
server - fix tests
aditya-arcot Mar 16, 2026
8bcaaf3
update overview
aditya-arcot Mar 16, 2026
ae38fe8
Merge pull request #96 from arcot-labs/dev
aditya-arcot Mar 16, 2026
3fff90f
fix migration downgrade
aditya-arcot Mar 16, 2026
ea7acdf
client - update deps
aditya-arcot Mar 16, 2026
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: 2 additions & 0 deletions .actrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--container-architecture linux/amd64
-P self-hosted=catthehacker/ubuntu:act-latest
5 changes: 5 additions & 0 deletions PROJECT_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ Basic relationships:
- **Exercise Library**: Authenticated users can browse system exercises, create their own exercises, and tag them with muscle groups.
- **Feedback**: Authenticated users submit feedback; server stores entry and can open a GitHub issue.

## Current Implementation Status

- ✅ Exercise API + client CRUD flow is implemented (`/api/exercises`, `/api/muscle-groups`, `/exercises` page).
- 🚧 Workout API + client workflow is the primary remaining feature area.

## API Surface (Current)

- `POST /api/auth/*`: request-access, register, login, refresh-token, logout, forgot/reset-password
Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
[![CI / Build Images](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml)
[![CD / Deploy Application](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml)

### Local Development
# RepTrack

## Local Development

Copy `.env.example` to `.env` & populate variables

Expand All @@ -25,8 +27,34 @@ Start containers:
./scripts/dev.sh
```

### Database
## Local GitHub Actions Testing

Use `act` to run workflows locally for quick validation.

List all workflows:

```bash
act -l
```

Run a specific job:

```bash
act -j {job-id}
```

## Database Conventions

All writes should go through SQLAlchemy

Alembic updates & bulk SQLAlchemy updates must explicitly set `updated_at`

## Shadcn Component Conventions

shadcn adds components under `client/src/components/ui/`

To ensure custom styles & behavior survive component updates, follow these conventions:

- Create custom component overrides under `client/src/components/ui/overrides/`
- Import override components in app code instead of generated shadcn components
- Add ESLint rules to prevent direct imports of generated components & point to override paths
5 changes: 5 additions & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export default defineConfig([
},
],
paths: [
{
name: '@/components/ui/button',
message:
'Use Button from @/components/ui/overrides/button',
},
{
name: 'sonner',
importNames: ['toast'],
Expand Down
215 changes: 114 additions & 101 deletions client/package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"generate-api": "openapi-ts",
"lint": "eslint --fix --max-warnings=0",
"preview": "vite preview",
"update-components": "for file in src/components/ui/*.tsx; do npx shadcn@latest add -o $(basename \"$file\" .tsx); done"
"update-components": "for file in src/components/ui/*.tsx; do npx shadcn@latest add -o $(basename \"$file\" .tsx); done && cd .. && npx prettier --write client/src/components/ui"
},
"lint-staged": {
"*": [
Expand All @@ -18,7 +18,7 @@
]
},
"dependencies": {
"@hey-api/openapi-ts": "^0.94.1",
"@hey-api/openapi-ts": "^0.94.2",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand Down Expand Up @@ -58,14 +58,14 @@
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react": "^5.2.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.57.0",
"typescript-eslint": "^8.57.1",
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.1.1"
}
Expand Down
2 changes: 2 additions & 0 deletions client/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppLayout } from '@/layout/AppLayout'
import { Admin } from '@/pages/Admin'
import { Dashboard } from '@/pages/Dashboard'
import { Docs } from '@/pages/Docs'
import { Exercises } from '@/pages/Exercises'
import { ForgotPassword } from '@/pages/ForgotPassword'
import { Login } from '@/pages/Login'
import { Register } from '@/pages/Register'
Expand All @@ -25,6 +26,7 @@ export function AppRoutes() {
}
>
<Route index element={<Dashboard />} />
<Route path="exercises" element={<Exercises />} />
<Route path="docs" element={<Docs />}>
<Route index element={<DocsIndex />} />
<Route path=":slug" element={<Doc />} />
Expand Down
4 changes: 2 additions & 2 deletions client/src/auth/RequireAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ interface RequireAuthProps {
}

export function RequireAuth({ children, requireAdmin }: RequireAuthProps) {
const { loading, authenticated, user } = useSession()
const { isLoading, authenticated, user } = useSession()
const location = useLocation()
if (loading) return <Loading />
if (isLoading) return <Loading />
if (!authenticated)
return <Navigate to="/login" replace state={{ from: location }} />
if (requireAdmin && !user?.is_admin) return <Navigate to="/" replace />
Expand Down
4 changes: 2 additions & 2 deletions client/src/auth/RequireGuest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { JSX } from 'react'
import { Navigate, useLocation } from 'react-router-dom'

export function RequireGuest({ children }: { children: JSX.Element }) {
const { loading, authenticated } = useSession()
const { isLoading, authenticated } = useSession()
const location = useLocation()
const state = location.state as LocationState | null
if (loading) return <Loading />
if (isLoading) return <Loading />
if (authenticated) {
const to = state?.from?.pathname ?? '/'
return <Navigate to={to} replace />
Expand Down
8 changes: 4 additions & 4 deletions client/src/auth/SessionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { type ReactNode, useEffect, useState } from 'react'

export function SessionProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<UserPublic | null>(null)
const [loading, setLoading] = useState(true)
const [isLoading, setIsLoading] = useState(true)

const loadSession = async () => {
setLoading(true)
setIsLoading(true)
try {
const { data, error } = await UserService.getCurrentUser()
if (error) {
Expand All @@ -18,7 +18,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
logger.info('Fetched current user', data)
setUser(data)
} finally {
setLoading(false)
setIsLoading(false)
}
}

Expand All @@ -30,7 +30,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
<SessionContext.Provider
value={{
user,
loading,
isLoading: isLoading,
authenticated: user !== null,
refresh: loadSession,
}}
Expand Down
69 changes: 29 additions & 40 deletions client/src/components/AccessRequestsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,23 @@ import { useSession } from '@/auth/session'
import { DataTable } from '@/components/data-table/DataTable'
import { DataTableColumnHeader } from '@/components/data-table/DataTableColumnHeader'
import { DataTableInlineRowActions } from '@/components/data-table/DataTableInlineRowActions'
import { createSelectColumn } from '@/components/data-table/DataTableSelectColumn'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/overrides/button'
import { handleApiError } from '@/lib/http'
import { notify } from '@/lib/notify'
import {
blueText,
greenBackground,
greenBackgroundHover,
greenText,
lightBlueBackground,
lightGreenBackground,
lightRedBackground,
redBackground,
redBackgroundHover,
redText,
} from '@/lib/styles'
import type {
Expand All @@ -44,8 +40,6 @@ import { useState } from 'react'
const blueBadgeClassName = `${lightBlueBackground} ${blueText}`
const greenBadgeClassName = `${lightGreenBackground} ${greenText}`
const redBadgeClassName = `${lightRedBackground} ${redText}`
const greenButtonClassName = `${greenBackground} ${greenBackgroundHover}`
const redButtonClassName = `${redBackground} ${redBackgroundHover}`

function StatusBadge({ status }: { status: AccessRequestStatus }) {
switch (status) {
Expand Down Expand Up @@ -79,7 +73,7 @@ export function AccessRequestsTable({
onReloadRequests,
}: AccessRequestsTableProps) {
const { user } = useSession()
const [loadingRequestIds, setLoadingRequestIds] = useState<Set<number>>(
const [isLoadingRequestIds, setIsLoadingRequestIds] = useState<Set<number>>(
new Set()
)
const [confirmDialog, setConfirmDialog] = useState<{
Expand All @@ -92,13 +86,7 @@ export function AccessRequestsTable({
action: null,
})

const handleConfirmAction = () => {
if (confirmDialog.request && confirmDialog.action)
void handleUpdateStatus(confirmDialog.request, confirmDialog.action)
setConfirmDialog({ isOpen: false, request: null, action: null })
}

const handleShowConfirmDialog = (
const openConfirmDialog = (
request: AccessRequestPublic,
action: 'approved' | 'rejected'
) => {
Expand All @@ -109,11 +97,25 @@ export function AccessRequestsTable({
})
}

const closeConfirmDialog = () => {
setConfirmDialog({
isOpen: false,
request: null,
action: null,
})
}

const handleConfirmAction = () => {
if (confirmDialog.request && confirmDialog.action)
void handleUpdateStatus(confirmDialog.request, confirmDialog.action)
closeConfirmDialog()
}

const handleUpdateStatus = async (
request: AccessRequestPublic,
status: 'approved' | 'rejected'
) => {
setLoadingRequestIds((prev) => new Set(prev).add(request.id))
setIsLoadingRequestIds((prev) => new Set(prev).add(request.id))
try {
const { error } = await AdminService.updateAccessRequestStatus({
path: {
Expand Down Expand Up @@ -148,7 +150,7 @@ export function AccessRequestsTable({
}
onRequestUpdated(updatedRequest)
} finally {
setLoadingRequestIds((prev) => {
setIsLoadingRequestIds((prev) => {
const next = new Set(prev)
next.delete(request.id)
return next
Expand All @@ -161,25 +163,23 @@ export function AccessRequestsTable({
menuItems: (row) => {
if (row.status !== 'pending') return []

const isRowLoading = loadingRequestIds.has(row.id)
const isRowLoading = isLoadingRequestIds.has(row.id)
return [
{
type: 'action',
label: 'Approve',
className: greenText,
icon: Check,
onSelect: () => {
handleShowConfirmDialog(row, 'approved')
openConfirmDialog(row, 'approved')
},
disabled: isRowLoading,
},
{
type: 'action',
className: redText,
label: 'Reject',
icon: X,
onSelect: () => {
handleShowConfirmDialog(row, 'rejected')
openConfirmDialog(row, 'rejected')
},
disabled: isRowLoading,
},
Expand All @@ -188,7 +188,6 @@ export function AccessRequestsTable({
}

const columns: ColumnDef<AccessRequestPublic>[] = [
createSelectColumn<AccessRequestPublic>(),
{
id: 'name',
accessorFn: (row) => `${row.first_name} ${row.last_name}`,
Expand Down Expand Up @@ -327,31 +326,21 @@ export function AccessRequestsTable({
?
<div className="mt-2">This action is irreversible.</div>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setConfirmDialog({
...confirmDialog,
isOpen: false,
})
}}
>
Cancel
</Button>
<DialogFooter>
<Button onClick={closeConfirmDialog}>Cancel</Button>
<Button
onClick={handleConfirmAction}
className={
variant={
confirmDialog.action === 'approved'
? greenButtonClassName
: redButtonClassName
? 'success'
: 'destructive'
}
>
{confirmDialog.action === 'approved'
? 'Approve'
: 'Reject'}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</>
Expand Down
Loading