Add avatar upload to Settings, document Settings page, update README

- Profile section: click avatar to upload a personal photo (FileReader
  preview, hover camera icon overlay), replacing static initials
- README.md rewritten with project-specific page status table, dev
  setup, tech stack, and deployment notes
- design-decisions.md: add Settings Page subsection documenting layout,
  section-switching, and the avatar upload technique

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
This commit is contained in:
Claude 2026-06-18 18:50:43 +00:00
parent e386e327b4
commit f24edb74b2
No known key found for this signature in database
3 changed files with 90 additions and 66 deletions

View file

@ -1,73 +1,39 @@
# React + TypeScript + Vite # ArchNest
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. A self-hosted ops dashboard — infrastructure monitoring, a bookmark hub for your homelab/cloud links, an embedded terminal, and system settings, all in one place.
Currently, two official plugins are available: Built with React 19 + TypeScript + Vite, styled with Tailwind CSS v4, charts via Recharts, icons via Lucide React.
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) ## Pages
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler | Page | Route | Status |
|------|-------|--------|
| Glance | `/` | Done — main ops dashboard (system status, resource overview, alerts, network traffic) |
| Infrastructure | `/infrastructure` | Done — resource distribution, node status grid, cost/trend breakdown. "Network" sub-tab planned as a future addition. |
| BookNest | `/booknest` | Done — categorized bookmark hub with quick access, favorites, link health, and category breakdown |
| Terminal | `/terminal` | Pending — will be based on a fork of the (archived) Termix project, not yet merged in |
| Settings | `/settings` | Done — Profile (incl. avatar upload), Appearance, Integrations, Notifications, Data & Backup, About |
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). See `archnest-blueprint.md` for the original per-page design spec and `design-decisions.md` for the visual/UX conventions and lessons learned while building each page — read that file before making layout changes, it documents *why* things are built the way they are (hero banner layering, card blend techniques, icon library gotchas, etc.).
## Expanding the ESLint configuration ## Development
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: ```bash
npm install
```js npm run dev
export default defineConfig([
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...
},
},
])
``` ```
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: Type-check with `npx tsc --noEmit` before committing — Vite/the browser surface some runtime errors (e.g. missing icon exports) that the type-checker won't catch, so also smoke-test pages in a browser.
```js ## Tech Stack
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([ - React 19 + Vite + TypeScript
globalIgnores(['dist']), - React Router for routing
{ - Tailwind CSS v4
files: ['**/*.{ts,tsx}'], - Recharts (donuts, line/area charts)
extends: [ - Lucide React (icons)
// Other configs... - Deploy target: Docker on racknerd1 → NPM proxy at archnest.snsnetlabs.com
// Enable lint rules for React
reactX.configs['recommended-typescript'], ## Deployment
// Enable lint rules for React DOM
reactDom.configs.recommended, This project is deployed via Docker on `racknerd1`, proxied through Nginx Proxy Manager at `archnest.snsnetlabs.com`.
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View file

@ -153,6 +153,32 @@
brand-flavored; substitutes used here: `GitBranch`/`GitFork` (GitHub/GitLab/Gitea), brand-flavored; substitutes used here: `GitBranch`/`GitFork` (GitHub/GitLab/Gitea),
`SquarePlay` (YouTube), `Briefcase` (LinkedIn Learning). `SquarePlay` (YouTube), `Briefcase` (LinkedIn Learning).
### Settings Page
- No mockup image existed for this page — built directly from the blueprint's Page 6
spec rather than iterating against a screenshot.
- Layout: fixed-width (200px) left nav listing the 6 sections (Profile, Appearance,
Integrations, Notifications, Data & Backup, About) + a scrollable content panel on
the right showing the active section. No hero banner (not in the blueprint spec for
this page, and a settings page doesn't need one).
- Active section is local component state (a string id) mapped through a
`sectionComponents` record to the corresponding section-renderer function — simplest
approach for a page with no routing/deep-linking requirement.
- Shared style helpers (`cardBase`, `sectionTitle`, `labelStyle`, `inputStyle`) plus two
small reusable components defined in the same file: `Toggle` (on/off pill switch) and
`GoldButton` (gold-filled primary / danger-outline variant) — kept local to
`Settings.tsx` rather than extracted, since no other page needs them yet.
- **Integrations** cards mask secret fields (API keys/tokens) behind dots with an eye
icon to reveal/hide, plus a "Test Connection" button per card — matches the blueprint's
explicit "masked secrets with eye toggle" instruction.
- **Avatar upload** (Profile section): the avatar circle is clickable and opens a hidden
`<input type="file" accept="image/*">` via a `useRef` + `.click()` call rather than a
visible file input — keeps the round avatar as the only visible control. On change,
`FileReader.readAsDataURL` converts the selected image to a base64 data URL stored in
component state, which becomes the circle's `backgroundImage` (cover-fit), replacing
the "AO" initials fallback. A hover-only camera-icon overlay (Tailwind `group` /
`group-hover:opacity-100`) signals the circle is clickable without cluttering the
default state. This is a frontend-only preview (no backend upload endpoint exists yet).
--- ---
## Future Integration Notes ## Future Integration Notes

View file

@ -1,4 +1,4 @@
import { useState } from 'react' import { useRef, useState } from 'react'
import { import {
User, User,
Palette, Palette,
@ -13,6 +13,7 @@ import {
Upload, Upload,
Trash2, Trash2,
RotateCcw, RotateCcw,
Camera,
} from 'lucide-react' } from 'lucide-react'
const navSections = [ const navSections = [
@ -133,16 +134,47 @@ function GoldButton({ children, danger }: { children: React.ReactNode; danger?:
} }
function ProfileSection() { function ProfileSection() {
const [avatar, setAvatar] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
function handleAvatarChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => setAvatar(reader.result as string)
reader.readAsDataURL(file)
}
return ( return (
<div style={cardBase}> <div style={cardBase}>
<h3 style={sectionTitle}>Profile</h3> <h3 style={sectionTitle}>Profile</h3>
<div className="flex items-center gap-4" style={{ marginBottom: '24px' }}> <div className="flex items-center gap-4" style={{ marginBottom: '24px' }}>
<div <div
className="rounded-full border-2 flex items-center justify-center font-bold" onClick={() => fileInputRef.current?.click()}
style={{ width: '64px', height: '64px', borderColor: '#C8A434', color: '#C8A434', fontSize: '20px', backgroundColor: 'rgba(200,164,52,0.08)' }} className="relative rounded-full border-2 flex items-center justify-center font-bold cursor-pointer group"
style={{
width: '64px',
height: '64px',
borderColor: '#C8A434',
color: '#C8A434',
fontSize: '20px',
backgroundColor: 'rgba(200,164,52,0.08)',
backgroundImage: avatar ? `url(${avatar})` : undefined,
backgroundSize: 'cover',
backgroundPosition: 'center',
overflow: 'hidden',
}}
title="Upload photo"
> >
AO {!avatar && 'AO'}
<div
className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
style={{ backgroundColor: 'rgba(0,0,0,0.55)' }}
>
<Camera size={18} color="#E8E6E0" />
</div>
</div> </div>
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleAvatarChange} className="hidden" />
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span style={{ fontSize: '15px', color: '#E8E6E0', fontWeight: 600 }}>ArchNest Ops</span> <span style={{ fontSize: '15px', color: '#E8E6E0', fontWeight: 600 }}>ArchNest Ops</span>