From ca1a76ce1597f6c99ae5c189137bf2ac78c19819 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 13:04:52 +0200 Subject: [PATCH 1/9] chore: remove unnecessary imports --- frontend/src/components/EditProjectDialog.jsx | 2 +- frontend/src/components/Header.jsx | 2 -- frontend/src/lib/AuthContext.jsx | 2 +- frontend/src/lib/utils.js | 2 -- frontend/src/screens/AllProjects.jsx | 11 ----------- frontend/src/screens/CreateScenario.jsx | 3 +-- frontend/src/screens/Report.jsx | 11 +---------- frontend/src/screens/Scenario.jsx | 1 - 8 files changed, 4 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/EditProjectDialog.jsx b/frontend/src/components/EditProjectDialog.jsx index 06e99b0..65df5d0 100644 --- a/frontend/src/components/EditProjectDialog.jsx +++ b/frontend/src/components/EditProjectDialog.jsx @@ -28,7 +28,7 @@ import LocationCombobox from "./LocationCombobox" export default function EditProjectDialog({ onSaved }) { const { projectId } = useParams(); const navigate = useNavigate() - const { data: project, loading: projLoading, error: projError } = useProject(projectId) + const { data: project } = useProject(projectId) const updateProject = useUpdateProject() const deleteProject = useDeleteProject() diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 997f677..cc39fda 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -4,8 +4,6 @@ import { useNavigate } from 'react-router' import { SettingsIcon, - BellIcon, - CreditCardIcon, LogOutIcon, } from "lucide-react" import { diff --git a/frontend/src/lib/AuthContext.jsx b/frontend/src/lib/AuthContext.jsx index 52fa9a3..112664b 100644 --- a/frontend/src/lib/AuthContext.jsx +++ b/frontend/src/lib/AuthContext.jsx @@ -1,4 +1,4 @@ -import React, { createContext, useState, useEffect } from 'react'; +import { createContext, useState, useEffect } from 'react'; export const AuthContext = createContext(); diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js index 19c5cd8..a9272bd 100644 --- a/frontend/src/lib/utils.js +++ b/frontend/src/lib/utils.js @@ -1,9 +1,7 @@ import { clsx } from "clsx"; import { twMerge } from "tailwind-merge" -import { useContext } from "react" import { jwtDecode } from "jwt-decode" -import { AuthContext } from './AuthContext' export function cn(...inputs) { return twMerge(clsx(inputs)); diff --git a/frontend/src/screens/AllProjects.jsx b/frontend/src/screens/AllProjects.jsx index fb56bc9..f428490 100644 --- a/frontend/src/screens/AllProjects.jsx +++ b/frontend/src/screens/AllProjects.jsx @@ -1,18 +1,8 @@ import { Link } from "react-router" import { useContext, useEffect } from "react" -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" import CreateProjectDialog from "../components/CreateProjectDialog" -import { Button } from "@/components/ui/button" import Footer from "../components/Footer" -import GuestAlert from "../components/GuestAlert"; import Header from "../components/Header"; import ProjectCard from "../components/ProjectCard" @@ -21,7 +11,6 @@ import { useProjects } from "../lib/useData" const AllProjects = () => { - const { isGuest } = useContext(AuthContext) const { data: projects, loading, error } = useProjects() useEffect(() => { diff --git a/frontend/src/screens/CreateScenario.jsx b/frontend/src/screens/CreateScenario.jsx index ba15f04..f9a22cb 100644 --- a/frontend/src/screens/CreateScenario.jsx +++ b/frontend/src/screens/CreateScenario.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router" +import { useNavigate, useParams } from "react-router" import { Breadcrumb, @@ -31,7 +31,6 @@ import { Input } from "@/components/ui/input" import { Table, TableBody, - TableCaption, TableCell, TableRow, } from "@/components/ui/table" diff --git a/frontend/src/screens/Report.jsx b/frontend/src/screens/Report.jsx index 02c9f83..77a0643 100644 --- a/frontend/src/screens/Report.jsx +++ b/frontend/src/screens/Report.jsx @@ -1,7 +1,6 @@ import { useContext, useEffect, useState } from "react" import { useParams } from "react-router" -import { Button } from "@/components/ui/button" import { Card, CardContent, @@ -10,9 +9,7 @@ import { } from "@/components/ui/card" import { ChartBarInteractive } from "../components/BarChart"; import Footer from "../components/Footer" -import Header from "../components/Header"; import Map from "../components/Map"; -import { Download, Pencil, Plus } from "lucide-react" import { Progress } from "@/components/ui/progress" import { Table, @@ -21,11 +18,6 @@ import { TableCell, TableRow, } from "@/components/ui/table" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" import { AuthContext } from "../lib/AuthContext" import Logo from "../assets/pyphos-logo.svg" @@ -34,10 +26,9 @@ import "../styles/print.css" export default function Report() { const { projectId, scenarioId } = useParams(); - const { token } = useContext(AuthContext) const { data: project, loading: projLoading, error: projError } = useApi(`/api/projects/${projectId}`) const { data: scenario, loading: scenLoading, error: scenError } = useApi(`/api/projects/${projectId}/scenarios/${scenarioId}`) - const { data: report, loading: reportLoading, error: reportError } = useApi(`/api/projects/${projectId}/scenarios/${scenarioId}/report`) + const { data: report } = useApi(`/api/projects/${projectId}/scenarios/${scenarioId}/report`) const [projectName, setProjectName] = useState("") const [projectLocation, setProjectLocation] = useState("") diff --git a/frontend/src/screens/Scenario.jsx b/frontend/src/screens/Scenario.jsx index 8880db4..7bd2257 100644 --- a/frontend/src/screens/Scenario.jsx +++ b/frontend/src/screens/Scenario.jsx @@ -42,7 +42,6 @@ import { Input } from "@/components/ui/input" import { Table, TableBody, - TableCaption, TableCell, TableRow, } from "@/components/ui/table" From e4e30dac61b73fb1fdf2e858f7d0a6893b0e1f17 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 13:45:24 +0200 Subject: [PATCH 2/9] chore: fix all set-state-in-effect lint errors --- frontend/src/components/EditProjectDialog.jsx | 30 +++++++------- frontend/src/components/LocationCombobox.jsx | 40 +++++++++++-------- frontend/src/lib/AuthContext.jsx | 17 ++++---- frontend/src/lib/api.js | 8 ++-- frontend/src/screens/CreateScenario.jsx | 12 ++---- frontend/src/screens/Project.jsx | 34 ++++++++-------- frontend/src/screens/Report.jsx | 19 +++++---- frontend/src/screens/Scenario.jsx | 32 ++++++--------- frontend/src/screens/Settings.jsx | 10 ++--- 9 files changed, 97 insertions(+), 105 deletions(-) diff --git a/frontend/src/components/EditProjectDialog.jsx b/frontend/src/components/EditProjectDialog.jsx index 65df5d0..ac67622 100644 --- a/frontend/src/components/EditProjectDialog.jsx +++ b/frontend/src/components/EditProjectDialog.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react" +import { useState } from "react" import { useNavigate, useParams } from "react-router" import { @@ -42,25 +42,25 @@ export default function EditProjectDialog({ onSaved }) { const [touched, setTouched] = useState({}) const [deleting, setDeleting] = useState(false) const [deleteError, setDeleteError] = useState(null) + const [loadedProjectId, setLoadedProjectId] = useState(null) const nameCheck = validateName(projectName, "Project name") const locationCheck = validateRequired(locationQuery, "Location") const isFormValid = nameCheck.valid && locationCheck.valid - useEffect(() => { - if (project) { - setProjectName(project.name ?? "") - setLocation({ - name: project.location, - country_code: project.country_code, - admin1: null, - lat: project.lat, - lon: project.lon, - }) - setLocationQuery(project.location ?? "") - setLocationChanged(false) - } - }, [project]) + if (project && project.id !== loadedProjectId) { + setLoadedProjectId(project.id) + setProjectName(project.name ?? "") + setLocation({ + name: project.location, + country_code: project.country_code, + admin1: null, + lat: project.lat, + lon: project.lon, + }) + setLocationQuery(project.location ?? "") + setLocationChanged(false) + } function markTouched(field) { setTouched((t) => ({ ...t, [field]: true })) diff --git a/frontend/src/components/LocationCombobox.jsx b/frontend/src/components/LocationCombobox.jsx index 55e662e..e4fafc5 100644 --- a/frontend/src/components/LocationCombobox.jsx +++ b/frontend/src/components/LocationCombobox.jsx @@ -37,28 +37,36 @@ export default function LocationCombobox({ id, value, onSelect, ariaInvalid, onB } useEffect(() => { - setQuery(formatLocation(value)) + function syncQuery() { + setQuery(formatLocation(value)) + } + + syncQuery() }, [value]) useEffect(() => { - if (!query.trim() || query === formatLocation(value)) { - setLoading(false) - return - } - - setLoading(true) - const timeout = setTimeout(async () => { - try { - const candidates = await searchLocations(token, query) - setResults(candidates ?? []) - } catch { - setResults([]) - } finally { + function search() { + if (!query.trim() || query === formatLocation(value)) { setLoading(false) + return } - }, DEBOUNCE_MS) - return () => clearTimeout(timeout) + setLoading(true) + const timeout = setTimeout(async () => { + try { + const candidates = await searchLocations(token, query) + setResults(candidates ?? []) + } catch { + setResults([]) + } finally { + setLoading(false) + } + }, DEBOUNCE_MS) + + return () => clearTimeout(timeout) + } + + return search() // eslint-disable-next-line react-hooks/exhaustive-deps }, [query, token]) diff --git a/frontend/src/lib/AuthContext.jsx b/frontend/src/lib/AuthContext.jsx index 112664b..b104b04 100644 --- a/frontend/src/lib/AuthContext.jsx +++ b/frontend/src/lib/AuthContext.jsx @@ -39,18 +39,15 @@ export const AuthProvider = ({ children }) => { }; useEffect(() => { - const storedToken = localStorage.getItem('token'); - if (storedToken) { - setToken(storedToken); + async function syncUser() { + if (!token || isGuest) { + setUser(null); + return; + } + setUser(await fetchCurrentUser(token)); } - }, []); - useEffect(() => { - if (!token || isGuest) { - setUser(null); - return; - } - fetchCurrentUser(token).then(setUser); + syncUser(); }, [token, isGuest]); return ( diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index ddd5a6e..d67b307 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -12,11 +12,11 @@ export function useApi(endpoint) { useEffect(() => { if (!endpoint) return - setLoading(true) - setData(null) - setError(null) - const fetchContent = async () => { + setLoading(true) + setData(null) + setError(null) + try { const response = await fetch(endpoint, { headers: token ? { Authorization: `Bearer ${token}` } : {} diff --git a/frontend/src/screens/CreateScenario.jsx b/frontend/src/screens/CreateScenario.jsx index f9a22cb..d5032f3 100644 --- a/frontend/src/screens/CreateScenario.jsx +++ b/frontend/src/screens/CreateScenario.jsx @@ -70,15 +70,9 @@ export default function CreateScenario() { const moduleAmountCheck = validateModuleAmount(moduleAmount) const isFormValid = nameCheck.valid && moduleAmountCheck.valid && !!selectedModule - useEffect(() => { - if (!modules || modules.length === 0) return - - const stillValid = modules.some((m) => m.model === selectedModel) - if (!stillValid) { - setSelectedModel(modules[0].model) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modules]) + if (modules && modules.length > 0 && !modules.some((m) => m.model === selectedModel)) { + setSelectedModel(modules[0].model) + } function markTouched(field) { setTouched((t) => ({ ...t, [field]: true })) diff --git a/frontend/src/screens/Project.jsx b/frontend/src/screens/Project.jsx index b152e27..fe3de7f 100644 --- a/frontend/src/screens/Project.jsx +++ b/frontend/src/screens/Project.jsx @@ -57,6 +57,7 @@ const Project = () => { const [projectLocation, setProjectLocation] = useState("") const [projectCountryCode, setProjectCountryCode] = useState("") const [projectCoords, setProjectCoords] = useState({ lat: null, lon: null }) + const [loadedProjectId, setLoadedProjectId] = useState(null) useEffect(() => { document.title = `Pyphos - ${projectName || "Project"}` @@ -64,23 +65,22 @@ const Project = () => { const [selectedScenario, setSelectedScenario] = useState(null) const [report, setReport] = useState("") + const [scenariosInitialized, setScenariosInitialized] = useState(false) - useEffect(() => { - if (project) { - setProjectName(project.name ?? "") - setProjectLocation(project.location ?? "") - setProjectCountryCode(project.country_code ?? "") - setProjectCoords({ lat: project.lat ?? null, lon: project.lon ?? null }) - } - }, [project]) + if (project && project.id !== loadedProjectId) { + setLoadedProjectId(project.id) + setProjectName(project.name ?? "") + setProjectLocation(project.location ?? "") + setProjectCountryCode(project.country_code ?? "") + setProjectCoords({ lat: project.lat ?? null, lon: project.lon ?? null }) + } - useEffect(() => { - if (scenarios && scenarios.length != 0) { - const preselected = location.state?.selectedScenarioId - && scenarios.find((s) => s.id === location.state.selectedScenarioId) - setSelectedScenario(preselected || scenarios.at(0)) - } - }, [scenarios]) + if (scenarios && scenarios.length != 0 && !scenariosInitialized) { + setScenariosInitialized(true) + const preselected = location.state?.selectedScenarioId + && scenarios.find((s) => s.id === location.state.selectedScenarioId) + setSelectedScenario(preselected || scenarios.at(0)) + } useEffect(() => { if (!selectedScenario) return @@ -89,7 +89,7 @@ const Project = () => { try { const data = await getReport(projectId, selectedScenario.id, project?.is_demo) setReport(data) - } catch (error) { + } catch (_error) { setReport(null) } } @@ -122,7 +122,7 @@ const Project = () => { document.body.removeChild(link) URL.revokeObjectURL(url) - } catch (error) { + } catch (_error) { toast.error("Failed to download report", { position: "top-center" }) } } diff --git a/frontend/src/screens/Report.jsx b/frontend/src/screens/Report.jsx index 77a0643..2390fc4 100644 --- a/frontend/src/screens/Report.jsx +++ b/frontend/src/screens/Report.jsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react" +import { useState } from "react" import { useParams } from "react-router" import { @@ -19,7 +19,6 @@ import { TableRow, } from "@/components/ui/table" -import { AuthContext } from "../lib/AuthContext" import Logo from "../assets/pyphos-logo.svg" import { useApi } from "../lib/api" import "../styles/print.css" @@ -34,15 +33,15 @@ export default function Report() { const [projectLocation, setProjectLocation] = useState("") const [projectCountryCode, setProjectCountryCode] = useState("") const [projectCoords, setProjectCoords] = useState({ lat: null, lon: null }) + const [loadedProjectId, setLoadedProjectId] = useState(null) - useEffect(() => { - if (project) { - setProjectName(project.name ?? "") - setProjectLocation(project.location ?? "") - setProjectCountryCode(project.country_code ?? "") - setProjectCoords({ lat: project.lat ?? null, lon: project.lon ?? null }) - } - }, [project]) + if (project && project.id !== loadedProjectId) { + setLoadedProjectId(project.id) + setProjectName(project.name ?? "") + setProjectLocation(project.location ?? "") + setProjectCountryCode(project.country_code ?? "") + setProjectCoords({ lat: project.lat ?? null, lon: project.lon ?? null }) + } if (projLoading || scenLoading) return

Loading...

if (projError || scenError) return

Something went wrong.

diff --git a/frontend/src/screens/Scenario.jsx b/frontend/src/screens/Scenario.jsx index 7bd2257..0644789 100644 --- a/frontend/src/screens/Scenario.jsx +++ b/frontend/src/screens/Scenario.jsx @@ -86,31 +86,25 @@ export default function Scenario() { const [deleting, setDeleting] = useState(false) const [deleteError, setDeleteError] = useState(null) + const [loadedScenarioId, setLoadedScenarioId] = useState(null) // avoid useEffect for resetting form fields when a new scenario loads function markTouched(field) { setTouched((t) => ({ ...t, [field]: true })) } - useEffect(() => { - if (scenario) { - setScenarioName(scenario.name ?? "") - setModuleAmount(scenario.module_amount ?? "") - setTilt(scenario.tilt ?? "") - setAzimuth(scenario.azimuth ?? "") - setSelectedManufacturer(scenario.module?.manufacturer ?? "") - setSelectedModel(scenario.module?.model ?? "") - } - }, [scenario]) - - useEffect(() => { - if (!modules || modules.length === 0) return + if (scenario && scenario.id !== loadedScenarioId) { + setLoadedScenarioId(scenario.id) + setScenarioName(scenario.name ?? "") + setModuleAmount(scenario.module_amount ?? "") + setTilt(scenario.tilt ?? "") + setAzimuth(scenario.azimuth ?? "") + setSelectedManufacturer(scenario.module?.manufacturer ?? "") + setSelectedModel(scenario.module?.model ?? "") + } - const stillValid = modules.some((m) => m.model === selectedModel) - if (!stillValid) { - setSelectedModel(modules[0].model) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [modules]) + if (modules && modules.length > 0 && !modules.some((m) => m.model === selectedModel)) { + setSelectedModel(modules[0].model) + } async function handleNameSubmit(e) { e?.preventDefault?.() diff --git a/frontend/src/screens/Settings.jsx b/frontend/src/screens/Settings.jsx index 4d8fe36..74e0f87 100644 --- a/frontend/src/screens/Settings.jsx +++ b/frontend/src/screens/Settings.jsx @@ -49,6 +49,7 @@ export default function Settings() { const [submitSuccess, setSubmitSuccess] = useState(false) const [deleting, setDeleting] = useState(false) const [deleteError, setDeleteError] = useState(null) + const [loadedUserId, setLoadedUserId] = useState(null) useEffect(() => { document.title = `Pyphos - Settings` @@ -58,11 +59,10 @@ export default function Settings() { const passwordCheck = password === "" ? { valid: true, message: null } : validatePassword(password) const isFormValid = emailCheck.valid && passwordCheck.valid - useEffect(() => { - if (user) { - setEmail(user.email ?? "") - } - }, [user]) + if (user && user.id !== loadedUserId) { + setLoadedUserId(user.id) + setEmail(user.email ?? "") + } function markTouched(field) { setTouched((t) => ({ ...t, [field]: true })) From a0eba2be9a4af1ffb1291fb95d4651af30db1fc0 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 15:22:38 +0200 Subject: [PATCH 3/9] chore: add underscore prefix for intentionally unused variables --- frontend/eslint.config.js | 7 +++++++ frontend/src/screens/AllProjects.jsx | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index ea36dd3..1f448ae 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -17,5 +17,12 @@ export default defineConfig([ globals: globals.browser, parserOptions: { ecmaFeatures: { jsx: true } }, }, + rules: { + 'no-unused-vars': ['error', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }], + }, }, ]) diff --git a/frontend/src/screens/AllProjects.jsx b/frontend/src/screens/AllProjects.jsx index f428490..a890d99 100644 --- a/frontend/src/screens/AllProjects.jsx +++ b/frontend/src/screens/AllProjects.jsx @@ -1,12 +1,11 @@ import { Link } from "react-router" -import { useContext, useEffect } from "react" +import { useEffect } from "react" import CreateProjectDialog from "../components/CreateProjectDialog" import Footer from "../components/Footer" import Header from "../components/Header"; import ProjectCard from "../components/ProjectCard" -import { AuthContext } from "../lib/AuthContext" import { useProjects } from "../lib/useData" From 095d84ddcb2cc08db9489cf42e483a793929b54e Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 15:54:22 +0200 Subject: [PATCH 4/9] fix: AuthContext into its own file --- frontend/src/App.jsx | 3 ++- frontend/src/components/Header.jsx | 2 +- frontend/src/components/LocationCombobox.jsx | 2 +- frontend/src/lib/AuthContext.jsx | 5 ++--- frontend/src/lib/api.js | 2 +- frontend/src/lib/auth-context.js | 3 +++ frontend/src/lib/useData.js | 2 +- frontend/src/screens/Landing.jsx | 2 +- frontend/src/screens/Login.jsx | 2 +- frontend/src/screens/Project.jsx | 3 +-- frontend/src/screens/Settings.jsx | 2 +- frontend/src/screens/Signup.jsx | 2 +- 12 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 frontend/src/lib/auth-context.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2fd4aea..0a9e73d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,7 +1,8 @@ import { useContext } from "react" import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router" -import { AuthProvider, AuthContext } from './lib/AuthContext'; +import { AuthProvider } from './lib/AuthContext'; +import { AuthContext } from './lib/auth-context'; import { isTokenValid } from './lib/utils'; import { Toaster } from '@/components/ui/sonner' import Landing from './screens/Landing' diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index cc39fda..73101ff 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -1,4 +1,4 @@ -import { AuthContext } from '@/lib/AuthContext' +import { AuthContext } from '@/lib/auth-context' import { useContext } from "react" import { useNavigate } from 'react-router' diff --git a/frontend/src/components/LocationCombobox.jsx b/frontend/src/components/LocationCombobox.jsx index e4fafc5..80902e0 100644 --- a/frontend/src/components/LocationCombobox.jsx +++ b/frontend/src/components/LocationCombobox.jsx @@ -9,7 +9,7 @@ import { ComboboxList, } from "@/components/ui/combobox" -import { AuthContext } from "../lib/AuthContext" +import { AuthContext } from "../lib/auth-context" import { searchLocations } from "../lib/api" const DEBOUNCE_MS = 300 diff --git a/frontend/src/lib/AuthContext.jsx b/frontend/src/lib/AuthContext.jsx index b104b04..727ce9d 100644 --- a/frontend/src/lib/AuthContext.jsx +++ b/frontend/src/lib/AuthContext.jsx @@ -1,6 +1,5 @@ -import { createContext, useState, useEffect } from 'react'; - -export const AuthContext = createContext(); +import { useState, useEffect } from 'react'; +import { AuthContext } from './auth-context'; async function fetchCurrentUser(token) { const response = await fetch('/api/users/me', { diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index d67b307..31d8bf0 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,5 +1,5 @@ import { useState, useEffect, useContext } from "react" -import { AuthContext } from "./AuthContext" +import { AuthContext } from "./auth-context" const BASE_URL = "/api" diff --git a/frontend/src/lib/auth-context.js b/frontend/src/lib/auth-context.js new file mode 100644 index 0000000..b957bc9 --- /dev/null +++ b/frontend/src/lib/auth-context.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const AuthContext = createContext(); diff --git a/frontend/src/lib/useData.js b/frontend/src/lib/useData.js index 5db3db7..c1aab6d 100644 --- a/frontend/src/lib/useData.js +++ b/frontend/src/lib/useData.js @@ -1,5 +1,5 @@ import { useContext, useMemo } from 'react' -import { AuthContext } from './AuthContext' +import { AuthContext } from './auth-context' import { useApi, createProject as createProjectApi, diff --git a/frontend/src/screens/Landing.jsx b/frontend/src/screens/Landing.jsx index 2965055..7c2093a 100644 --- a/frontend/src/screens/Landing.jsx +++ b/frontend/src/screens/Landing.jsx @@ -5,7 +5,7 @@ import { ArrowRight } from "lucide-react" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" -import { AuthContext } from "../lib/AuthContext" +import { AuthContext } from "../lib/auth-context" import Elsa from "../assets/avatar-elsa.jpg" import Logo from "../assets/pyphos-logo.svg" diff --git a/frontend/src/screens/Login.jsx b/frontend/src/screens/Login.jsx index 58d2947..dd71991 100644 --- a/frontend/src/screens/Login.jsx +++ b/frontend/src/screens/Login.jsx @@ -2,7 +2,7 @@ import { useState, useContext, useEffect } from 'react' import { Link, useNavigate } from 'react-router' import { ArrowLeft } from 'lucide-react' -import { AuthContext } from '@/lib/AuthContext' +import { AuthContext } from '@/lib/auth-context' import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { diff --git a/frontend/src/screens/Project.jsx b/frontend/src/screens/Project.jsx index fe3de7f..fc46839 100644 --- a/frontend/src/screens/Project.jsx +++ b/frontend/src/screens/Project.jsx @@ -38,7 +38,7 @@ import { } from "@/components/ui/tooltip" import { toast } from "sonner" -import { AuthContext } from "../lib/AuthContext" +import { AuthContext } from "../lib/auth-context" import { downloadReport } from "../lib/api" import { useProject, useReport, useScenarios } from "../lib/useData" import { getCompassDirection } from "../components/AngleSlider" @@ -93,7 +93,6 @@ const Project = () => { setReport(null) } } - fetchReport() }, [selectedScenario]) diff --git a/frontend/src/screens/Settings.jsx b/frontend/src/screens/Settings.jsx index 74e0f87..8f6b099 100644 --- a/frontend/src/screens/Settings.jsx +++ b/frontend/src/screens/Settings.jsx @@ -33,7 +33,7 @@ import Header from "../components/Header" import { Input } from "@/components/ui/input" import { Trash2 } from "lucide-react" -import { AuthContext } from "../lib/AuthContext" +import { AuthContext } from "../lib/auth-context" import { deleteUser, updateUser } from "../lib/api" import { validateEmail, validatePassword } from "../lib/validators" diff --git a/frontend/src/screens/Signup.jsx b/frontend/src/screens/Signup.jsx index f3d8497..c8ddf40 100644 --- a/frontend/src/screens/Signup.jsx +++ b/frontend/src/screens/Signup.jsx @@ -2,7 +2,7 @@ import { useState, useContext, useEffect } from 'react' import { Link, useNavigate } from 'react-router' import { ArrowLeft, InfoIcon } from 'lucide-react' -import { AuthContext } from '@/lib/AuthContext' +import { AuthContext } from '@/lib/auth-context' import { Button } from "@/components/ui/button" import { Card, CardContent } from "@/components/ui/card" import { From e1e4e915d94eb1c90feaf80195f6619d36bc12bd Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 16:15:51 +0200 Subject: [PATCH 5/9] fix: update requirements.txt, fix viteconfig --- backend/requirements.txt | 3 ++- frontend/vite.config.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 61536f8..6932514 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,4 +23,5 @@ argon2-cffi-bindings==25.1.0 cffi==2.0.0 pycparser==3.0 playwright==1.61.0 -starlette==1.0.0 \ No newline at end of file +starlette==1.0.0 +pytest==9.0.3 \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c4991cc..383db20 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -16,7 +16,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(import.meta.dirname, './src'), }, }, }) From 14fe7c4cd5412274ac213a3aaf6ab211a4346c70 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 16:16:17 +0200 Subject: [PATCH 6/9] update: add ci workflow --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5ddbd1e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: PyPhos CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + backend-tests: + runs-on: ubuntu-latest + + # Dummy values, just enough for the app to import and for the test suite + # to run. The test suite never touches DATABASE_URL for real - it's + # entirely SQLite in-memory (see backend/tests/conftest.py) - this only + # needs to exist so backend/utils/database.py doesn't raise at import time. + env: + DATABASE_URL: sqlite:///./ci-placeholder.db + SECRET_KEY: ci-test-secret-key + ALGORITHM: HS256 + ACCESS_TOKEN_EXPIRE_MINUTES: "30" + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install backend dependencies + run: | + pip install -r backend/requirements.txt + pip install pytest + + - name: Run backend tests + run: python -m pytest -v + + frontend-build: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: frontend + run: npm ci + + - name: Lint + working-directory: frontend + run: npm run lint + + - name: Build + working-directory: frontend + run: npm run build From 6329b2ff0dc93d7652d550a9c73af6006a5f0c0f Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 16:39:26 +0200 Subject: [PATCH 7/9] fix: separate dev requirements file --- .github/workflows/ci.yml | 6 ++---- backend/requirements-dev.txt | 3 +++ 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 backend/requirements-dev.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ddbd1e..51a013f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: PyPhos CI +name: pyphos-ci on: push: @@ -30,9 +30,7 @@ jobs: python-version: "3.11" - name: Install backend dependencies - run: | - pip install -r backend/requirements.txt - pip install pytest + run: pip install -r backend/requirements-dev.txt - name: Run backend tests run: python -m pytest -v diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 0000000..eacf1ed --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,3 @@ +-r requirements.txt +pytest==9.0.3 +httpx==0.28.1 From 3fc98e3dbdefa8f88a1f20bf0e30ea7ae447518d Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 16:51:07 +0200 Subject: [PATCH 8/9] fix: disable only-export-components lint rule for shadcn components --- frontend/eslint.config.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 1f448ae..d1b7e66 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -25,4 +25,13 @@ export default defineConfig([ }], }, }, + { + // Vendored shadcn/ui components: many co-export a variants function + // (e.g. buttonVariants) alongside their component by design, which + // trips this rule. That's expected here, not an app bug. + files: ['src/components/ui/**/*.{js,jsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + }, + }, ]) From 3e6ec760d25d4528c508299f5c710b55142eb950 Mon Sep 17 00:00:00 2001 From: Elsa Date: Fri, 21 Aug 2026 16:52:30 +0200 Subject: [PATCH 9/9] chore: remove unused imports, move getCompassDirection to lib/utils --- frontend/src/components/AngleSlider.jsx | 8 +------- frontend/src/components/ui/alert-dialog.jsx | 1 - frontend/src/components/ui/avatar.jsx | 1 - frontend/src/components/ui/breadcrumb.jsx | 1 - frontend/src/components/ui/card.jsx | 2 -- frontend/src/components/ui/dialog.jsx | 1 - frontend/src/components/ui/dropdown-menu.jsx | 1 - frontend/src/components/ui/input-group.jsx | 1 - frontend/src/components/ui/input.jsx | 1 - frontend/src/components/ui/label.jsx | 2 -- frontend/src/components/ui/table.jsx | 2 -- frontend/src/components/ui/textarea.jsx | 2 -- frontend/src/lib/utils.js | 7 +++++++ frontend/src/screens/Project.jsx | 2 +- 14 files changed, 9 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/AngleSlider.jsx b/frontend/src/components/AngleSlider.jsx index 6f664c5..18607aa 100644 --- a/frontend/src/components/AngleSlider.jsx +++ b/frontend/src/components/AngleSlider.jsx @@ -2,14 +2,8 @@ import { Label } from "@/components/ui/label" import { Slider } from "@/components/ui/slider" +import { getCompassDirection } from "@/lib/utils" -// Azimuth convention used across the app: South is 0°, West is positive, East is negative. -const COMPASS_DIRECTIONS = ["S", "SW", "W", "NW", "N", "NE", "E", "SE"] - -export function getCompassDirection(azimuth) { - const normalized = ((azimuth % 360) + 360) % 360 - return COMPASS_DIRECTIONS[Math.round(normalized / 45) % 8] -} export function AngleSlider({ name, min, max, value, onValueChange, tag }) { return ( diff --git a/frontend/src/components/ui/alert-dialog.jsx b/frontend/src/components/ui/alert-dialog.jsx index 726ac90..f387b9a 100644 --- a/frontend/src/components/ui/alert-dialog.jsx +++ b/frontend/src/components/ui/alert-dialog.jsx @@ -1,6 +1,5 @@ "use client" -import * as React from "react" import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/avatar.jsx b/frontend/src/components/ui/avatar.jsx index fee91c8..944bb9e 100644 --- a/frontend/src/components/ui/avatar.jsx +++ b/frontend/src/components/ui/avatar.jsx @@ -1,4 +1,3 @@ -import * as React from "react" import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar" import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/breadcrumb.jsx b/frontend/src/components/ui/breadcrumb.jsx index db2b171..5c1ca57 100644 --- a/frontend/src/components/ui/breadcrumb.jsx +++ b/frontend/src/components/ui/breadcrumb.jsx @@ -1,4 +1,3 @@ -import * as React from "react" import { mergeProps } from "@base-ui/react/merge-props" import { useRender } from "@base-ui/react/use-render" diff --git a/frontend/src/components/ui/card.jsx b/frontend/src/components/ui/card.jsx index d9c9916..cec0a8b 100644 --- a/frontend/src/components/ui/card.jsx +++ b/frontend/src/components/ui/card.jsx @@ -1,5 +1,3 @@ -import * as React from "react" - import { cn } from "@/lib/utils" function Card({ diff --git a/frontend/src/components/ui/dialog.jsx b/frontend/src/components/ui/dialog.jsx index bc425ea..cbcd91d 100644 --- a/frontend/src/components/ui/dialog.jsx +++ b/frontend/src/components/ui/dialog.jsx @@ -1,4 +1,3 @@ -import * as React from "react" import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/dropdown-menu.jsx b/frontend/src/components/ui/dropdown-menu.jsx index 2695da9..c97706b 100644 --- a/frontend/src/components/ui/dropdown-menu.jsx +++ b/frontend/src/components/ui/dropdown-menu.jsx @@ -1,4 +1,3 @@ -import * as React from "react" import { Menu as MenuPrimitive } from "@base-ui/react/menu" import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/input-group.jsx b/frontend/src/components/ui/input-group.jsx index 48d0727..ab0db86 100644 --- a/frontend/src/components/ui/input-group.jsx +++ b/frontend/src/components/ui/input-group.jsx @@ -1,6 +1,5 @@ "use client" -import * as React from "react" import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/input.jsx b/frontend/src/components/ui/input.jsx index d6ff8e4..0836a95 100644 --- a/frontend/src/components/ui/input.jsx +++ b/frontend/src/components/ui/input.jsx @@ -1,4 +1,3 @@ -import * as React from "react" import { Input as InputPrimitive } from "@base-ui/react/input" import { cn } from "@/lib/utils" diff --git a/frontend/src/components/ui/label.jsx b/frontend/src/components/ui/label.jsx index 773076b..5c6413d 100644 --- a/frontend/src/components/ui/label.jsx +++ b/frontend/src/components/ui/label.jsx @@ -1,7 +1,5 @@ "use client" -import * as React from "react" - import { cn } from "@/lib/utils" function Label({ diff --git a/frontend/src/components/ui/table.jsx b/frontend/src/components/ui/table.jsx index 8813c82..8cd2b5e 100644 --- a/frontend/src/components/ui/table.jsx +++ b/frontend/src/components/ui/table.jsx @@ -1,7 +1,5 @@ "use client" -import * as React from "react" - import { cn } from "@/lib/utils" function Table({ diff --git a/frontend/src/components/ui/textarea.jsx b/frontend/src/components/ui/textarea.jsx index decf453..6b47f71 100644 --- a/frontend/src/components/ui/textarea.jsx +++ b/frontend/src/components/ui/textarea.jsx @@ -1,5 +1,3 @@ -import * as React from "react" - import { cn } from "@/lib/utils" function Textarea({ diff --git a/frontend/src/lib/utils.js b/frontend/src/lib/utils.js index a9272bd..6944da3 100644 --- a/frontend/src/lib/utils.js +++ b/frontend/src/lib/utils.js @@ -15,4 +15,11 @@ export function isTokenValid(token) { } catch { return false } +} + +export function getCompassDirection(azimuth) { + // Azimuth convention used across the app: South is 0°, West is positive, East is negative. + const COMPASS_DIRECTIONS = ["S", "SW", "W", "NW", "N", "NE", "E", "SE"] + const normalized = ((azimuth % 360) + 360) % 360 + return COMPASS_DIRECTIONS[Math.round(normalized / 45) % 8] } \ No newline at end of file diff --git a/frontend/src/screens/Project.jsx b/frontend/src/screens/Project.jsx index fc46839..61a646f 100644 --- a/frontend/src/screens/Project.jsx +++ b/frontend/src/screens/Project.jsx @@ -41,7 +41,7 @@ import { toast } from "sonner" import { AuthContext } from "../lib/auth-context" import { downloadReport } from "../lib/api" import { useProject, useReport, useScenarios } from "../lib/useData" -import { getCompassDirection } from "../components/AngleSlider" +import { getCompassDirection } from "../lib/utils"