From f24edb74b21e6d49a6a41e7d1d2b686ec8c99180 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 18:50:43 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF --- README.md | 90 +++++++++++++----------------------------- design-decisions.md | 26 ++++++++++++ src/pages/Settings.tsx | 40 +++++++++++++++++-- 3 files changed, 90 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 7dbf7eb..450f1bf 100644 --- a/README.md +++ b/README.md @@ -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) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +## Pages -## 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: - -```js -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... - }, - }, -]) +```bash +npm install +npm run dev ``` -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 -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +## Tech Stack -export default defineConfig([ - 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... - }, - }, -]) -``` +- React 19 + Vite + TypeScript +- React Router for routing +- Tailwind CSS v4 +- Recharts (donuts, line/area charts) +- Lucide React (icons) +- Deploy target: Docker on racknerd1 → NPM proxy at archnest.snsnetlabs.com + +## Deployment + +This project is deployed via Docker on `racknerd1`, proxied through Nginx Proxy Manager at `archnest.snsnetlabs.com`. diff --git a/design-decisions.md b/design-decisions.md index 4f26945..dc97fed 100644 --- a/design-decisions.md +++ b/design-decisions.md @@ -153,6 +153,32 @@ brand-flavored; substitutes used here: `GitBranch`/`GitFork` (GitHub/GitLab/Gitea), `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 + `` 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 diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 8522658..2655a49 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { User, Palette, @@ -13,6 +13,7 @@ import { Upload, Trash2, RotateCcw, + Camera, } from 'lucide-react' const navSections = [ @@ -133,16 +134,47 @@ function GoldButton({ children, danger }: { children: React.ReactNode; danger?: } function ProfileSection() { + const [avatar, setAvatar] = useState(null) + const fileInputRef = useRef(null) + + function handleAvatarChange(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = () => setAvatar(reader.result as string) + reader.readAsDataURL(file) + } + return (

Profile

fileInputRef.current?.click()} + 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'} +
+ +
+
ArchNest Ops