Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ jobs:
working-directory: frontend
run: npm ci

- name: Check formatting
working-directory: frontend
run: npm run format:check

- name: Lint
working-directory: frontend
run: npm run lint
Expand Down
20 changes: 10 additions & 10 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ Talks to the [PyPhos backend API](../backend/README.md) for authentication, proj

## Screens

| Screen | Route | Description |
|---|---|---|
| `Login` | `/login` | Sign in |
| `Signup` | `/signup` | Create an account |
| `AllProjects` | `/projects` | List and create projects |
| `Project` | `/projects/:projectId` | View a project's details, its scenarios and their reports |
| `CreateScenario` | `/projects/:projectId/scenarios/create` | Configure a new scenario |
| `Scenario` | `/projects/:projectId/scenarios/:scenarioId` | Edit scenario details |
| `Settings` | `/settings` | Manage account settings |
| Screen | Route | Description |
| ---------------- | -------------------------------------------- | --------------------------------------------------------- |
| `Login` | `/login` | Sign in |
| `Signup` | `/signup` | Create an account |
| `AllProjects` | `/projects` | List and create projects |
| `Project` | `/projects/:projectId` | View a project's details, its scenarios and their reports |
| `CreateScenario` | `/projects/:projectId/scenarios/create` | Configure a new scenario |
| `Scenario` | `/projects/:projectId/scenarios/:scenarioId` | Edit scenario details |
| `Settings` | `/settings` | Manage account settings |

All routes except `/login` and `/signup` are behind `PrivateRoute`, which checks for a valid JWT and redirects to `/login` otherwise.

Expand All @@ -48,4 +48,4 @@ npm install
npm run dev
```

The app will be available at `http://localhost:5173`. Requests to `/api/*` are proxied to `http://localhost:8000` in development (see `vite.config.js`), and to the deployed backend in production (see `vercel.json`).
The app will be available at `http://localhost:5173`. Requests to `/api/*` are proxied to `http://localhost:8000` in development (see `vite.config.js`), and to the deployed backend in production (see `vercel.json`).
33 changes: 18 additions & 15 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config";

export default defineConfig([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{js,jsx}'],
files: ["**/*.{js,jsx}"],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
Expand All @@ -18,20 +18,23 @@ export default defineConfig([
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
'no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
// 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}'],
files: ["src/components/ui/**/*.{js,jsx}"],
rules: {
'react-refresh/only-export-components': 'off',
"react-refresh/only-export-components": "off",
},
},
])
]);
6 changes: 3 additions & 3 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyPhos</title>
</head>
Expand Down
2 changes: 1 addition & 1 deletion frontend/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
// "@/*": ["./src/*"]
// }
// }
// }
// }
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"format": "prettier . --write",
"format:check": "prettier . --check"
},
"dependencies": {
"@base-ui/react": "^1.6.0",
Expand Down Expand Up @@ -46,6 +48,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"prettier": "3.9.6",
"vite": "^8.0.12"
}
}
114 changes: 78 additions & 36 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,91 @@
import { useContext } from "react"
import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router"
import { useContext } from "react";
import { BrowserRouter as Router, Route, Routes, Navigate } from "react-router";

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'
import Login from './screens/Login'
import Signup from './screens/Signup'
import AllProjects from './screens/AllProjects'
import Project from './screens/Project'
import CreateScenario from './screens/CreateScenario'
import Report from './screens/Report'
import Scenario from './screens/Scenario'
import Settings from './screens/Settings'
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";
import Login from "./screens/Login";
import Signup from "./screens/Signup";
import AllProjects from "./screens/AllProjects";
import Project from "./screens/Project";
import CreateScenario from "./screens/CreateScenario";
import Report from "./screens/Report";
import Scenario from "./screens/Scenario";
import Settings from "./screens/Settings";

import './App.css'
import "./App.css";

export default function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/settings" element={<PrivateRoute><Settings /></PrivateRoute>} />
<Route path="/projects" element={<GuestOrPrivateRoute><AllProjects /></GuestOrPrivateRoute>} />
<Route path="/projects/:projectId" element={<GuestOrPrivateRoute><Project /></GuestOrPrivateRoute>} />
<Route path="/projects/:projectId/scenarios/create" element={<GuestOrPrivateRoute><CreateScenario /></GuestOrPrivateRoute>} />
<Route path="/projects/:projectId/scenarios/:scenarioId" element={<GuestOrPrivateRoute><Scenario /></GuestOrPrivateRoute>} />
<Route path="/projects/:projectId/scenarios/:scenarioId/report" element={<PrivateRoute><Report /></PrivateRoute>} />
</Routes>
</Router>
<Toaster />
</AuthProvider>
)
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route
path="/settings"
element={
<PrivateRoute>
<Settings />
</PrivateRoute>
}
/>
<Route
path="/projects"
element={
<GuestOrPrivateRoute>
<AllProjects />
</GuestOrPrivateRoute>
}
/>
<Route
path="/projects/:projectId"
element={
<GuestOrPrivateRoute>
<Project />
</GuestOrPrivateRoute>
}
/>
<Route
path="/projects/:projectId/scenarios/create"
element={
<GuestOrPrivateRoute>
<CreateScenario />
</GuestOrPrivateRoute>
}
/>
<Route
path="/projects/:projectId/scenarios/:scenarioId"
element={
<GuestOrPrivateRoute>
<Scenario />
</GuestOrPrivateRoute>
}
/>
<Route
path="/projects/:projectId/scenarios/:scenarioId/report"
element={
<PrivateRoute>
<Report />
</PrivateRoute>
}
/>
</Routes>
</Router>
<Toaster />
</AuthProvider>
);
}

const GuestOrPrivateRoute = ({ children }) => {
const { isGuest, token } = useContext(AuthContext);
return isGuest || isTokenValid(token) ? children : <Navigate to="/login" />
return isGuest || isTokenValid(token) ? children : <Navigate to="/login" />;
};

const PrivateRoute = ({ children }) => {
const { token } = useContext(AuthContext);
return isTokenValid(token) ? children : <Navigate to="/login" />
return isTokenValid(token) ? children : <Navigate to="/login" />;
};
11 changes: 5 additions & 6 deletions frontend/src/components/AngleSlider.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client"

import { Label } from "@/components/ui/label"
import { Slider } from "@/components/ui/slider"
import { getCompassDirection } from "@/lib/utils"
"use client";

import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { getCompassDirection } from "@/lib/utils";

export function AngleSlider({ name, min, max, value, onValueChange, tag }) {
return (
Expand All @@ -22,5 +21,5 @@ export function AngleSlider({ name, min, max, value, onValueChange, tag }) {
max={max}
/>
</div>
)
);
}
30 changes: 16 additions & 14 deletions frontend/src/components/BarChart.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
"use client"
"use client";

import * as React from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
import * as React from "react";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";

import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
} from "@/components/ui/chart";

export const description = "An interactive bar chart"
export const description = "An interactive bar chart";

const chartConfig = {
yield: {
Expand All @@ -27,24 +27,24 @@ const chartConfig = {
label: "Solar radiation",
color: "var(--chart-1)",
},
}
};

export function ChartBarInteractive({ data, height = 250 }) {
const [activeChart, setActiveChart] = React.useState("yield")
const [activeChart, setActiveChart] = React.useState("yield");

const totals = React.useMemo(
() => ({
yield: data?.reduce((acc, curr) => acc + curr.yield, 0) ?? 0,
radiation: data?.reduce((acc, curr) => acc + curr.radiation, 0) ?? 0,
}),
[data]
)
[data],
);

if (!data || data.length === 0) {
return null
return null;
}

const units = { yield: "kWh", radiation: "kWh/m²" }
const units = { yield: "kWh", radiation: "kWh/m²" };

return (
<Card className="py-0">
Expand All @@ -67,7 +67,9 @@ export function ChartBarInteractive({ data, height = 250 }) {
{chartConfig[key].label}
</span>
<span className="text-lg leading-none font-bold sm:text-3xl">
{totals[key].toLocaleString(undefined, { maximumFractionDigits: 0 })}
{totals[key].toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
<span className="ml-1 text-xs font-normal text-muted-foreground">
{units[key]}
</span>
Expand Down Expand Up @@ -114,5 +116,5 @@ export function ChartBarInteractive({ data, height = 250 }) {
</ChartContainer>
</CardContent>
</Card>
)
);
}
Loading
Loading