Swapped the entire UI to Material UI, applied a global dark theme, and removed all of the old styled-jsx/CSS-module styling

This commit is contained in:
fuomag9
2025-10-31 21:03:02 +01:00
parent 315192fb54
commit 29acf06f75
41 changed files with 3122 additions and 2445 deletions
@@ -0,0 +1,140 @@
"use client";
import {
Box,
Button,
Card,
CardContent,
Divider,
IconButton,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
Stack,
TextField,
Typography
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import type { AccessList } from "@/src/lib/models/access-lists";
import {
addAccessEntryAction,
createAccessListAction,
deleteAccessEntryAction,
deleteAccessListAction,
updateAccessListAction
} from "./actions";
type Props = {
lists: AccessList[];
};
export default function AccessListsClient({ lists }: Props) {
return (
<Stack spacing={4} sx={{ width: "100%" }}>
<Stack spacing={1}>
<Typography variant="h4" fontWeight={600}>
Access Lists
</Typography>
<Typography color="text.secondary">Protect proxy hosts with HTTP basic authentication credentials.</Typography>
</Stack>
<Stack spacing={3}>
{lists.map((list) => (
<Card key={list.id}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Stack component="form" action={(formData) => updateAccessListAction(list.id, formData)} spacing={2}>
<Typography variant="h6" fontWeight={600}>
Access List
</Typography>
<TextField name="name" label="Name" defaultValue={list.name} fullWidth />
<TextField
name="description"
label="Description"
defaultValue={list.description ?? ""}
multiline
minRows={2}
fullWidth
/>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
<Button type="submit" variant="contained">
Save
</Button>
<Button
type="submit"
formAction={deleteAccessListAction.bind(null, list.id)}
variant="outlined"
color="error"
>
Delete list
</Button>
</Box>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack spacing={1.5}>
<Typography fontWeight={600}>Accounts</Typography>
{list.entries.length === 0 ? (
<Typography color="text.secondary">No credentials configured.</Typography>
) : (
<List dense disablePadding>
{list.entries.map((entry) => (
<ListItem key={entry.id} sx={{ bgcolor: "background.default", borderRadius: 2, mb: 1 }}>
<ListItemText primary={entry.username} secondary={`Created ${new Date(entry.created_at).toLocaleDateString()}`} />
<ListItemSecondaryAction>
<form action={deleteAccessEntryAction.bind(null, list.id, entry.id)}>
<IconButton type="submit" edge="end" color="error">
<DeleteIcon fontSize="small" />
</IconButton>
</form>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
)}
</Stack>
<Divider sx={{ my: 1 }} />
<Stack component="form" action={(formData) => addAccessEntryAction(list.id, formData)} spacing={1.5} direction={{ xs: "column", sm: "row" }}>
<TextField name="username" label="Username" required fullWidth />
<TextField name="password" label="Password" type="password" required fullWidth />
<Button type="submit" variant="contained">
Add
</Button>
</Stack>
</CardContent>
</Card>
))}
</Stack>
<Stack spacing={2} component="section">
<Typography variant="h6" fontWeight={600}>
Create access list
</Typography>
<Card>
<CardContent>
<Stack component="form" action={createAccessListAction} spacing={2}>
<TextField name="name" label="Name" placeholder="Internal users" required fullWidth />
<TextField name="description" label="Description" placeholder="Optional description" multiline minRows={2} fullWidth />
<TextField
name="users"
label="Seed members"
helperText="One per line, username:password"
multiline
minRows={3}
fullWidth
/>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<Button type="submit" variant="contained">
Create Access List
</Button>
</Box>
</Stack>
</CardContent>
</Card>
</Stack>
</Stack>
);
}
+5 -5
View File
@@ -11,7 +11,7 @@ import {
} from "@/src/lib/models/access-lists";
export async function createAccessListAction(formData: FormData) {
const { user } = requireUser();
const { user } = await requireUser();
const rawUsers = String(formData.get("users") ?? "");
const accounts = rawUsers
.split("\n")
@@ -35,7 +35,7 @@ export async function createAccessListAction(formData: FormData) {
}
export async function updateAccessListAction(id: number, formData: FormData) {
const { user } = requireUser();
const { user } = await requireUser();
await updateAccessList(
id,
{
@@ -48,13 +48,13 @@ export async function updateAccessListAction(id: number, formData: FormData) {
}
export async function deleteAccessListAction(id: number) {
const { user } = requireUser();
const { user } = await requireUser();
await deleteAccessList(id, user.id);
revalidatePath("/access-lists");
}
export async function addAccessEntryAction(id: number, formData: FormData) {
const { user } = requireUser();
const { user } = await requireUser();
await addAccessListEntry(
id,
{
@@ -67,7 +67,7 @@ export async function addAccessEntryAction(id: number, formData: FormData) {
}
export async function deleteAccessEntryAction(accessListId: number, entryId: number) {
const { user } = requireUser();
const { user } = await requireUser();
await removeAccessListEntry(accessListId, entryId, user.id);
revalidatePath("/access-lists");
}
+2 -202
View File
@@ -1,207 +1,7 @@
import AccessListsClient from "./AccessListsClient";
import { listAccessLists } from "@/src/lib/models/access-lists";
import { addAccessEntryAction, createAccessListAction, deleteAccessEntryAction, deleteAccessListAction, updateAccessListAction } from "./actions";
export default function AccessListsPage() {
const lists = listAccessLists();
return (
<div className="page">
<header>
<h1>Access Lists</h1>
<p>Protect proxy hosts with HTTP basic authentication credentials.</p>
</header>
<section className="grid">
{lists.map((list) => (
<div className="card" key={list.id}>
<form action={(formData) => updateAccessListAction(list.id, formData)} className="header">
<div>
<input name="name" defaultValue={list.name} />
<textarea name="description" defaultValue={list.description ?? ""} rows={2} placeholder="Description" />
</div>
<button type="submit" className="primary small">
Save
</button>
</form>
<div className="entries">
<h3>Accounts</h3>
{list.entries.length === 0 ? (
<p className="empty">No credentials configured.</p>
) : (
<ul>
{list.entries.map((entry) => (
<li key={entry.id}>
<span>{entry.username}</span>
<form action={() => deleteAccessEntryAction(list.id, entry.id)}>
<button type="submit" className="ghost">
Remove
</button>
</form>
</li>
))}
</ul>
)}
</div>
<form action={(formData) => addAccessEntryAction(list.id, formData)} className="add-entry">
<input name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit" className="primary small">
Add
</button>
</form>
<form action={() => deleteAccessListAction(list.id)}>
<button type="submit" className="danger">
Delete list
</button>
</form>
</div>
))}
</section>
<section className="create">
<h2>Create access list</h2>
<form action={createAccessListAction} className="form">
<label>
Name
<input name="name" placeholder="Internal users" required />
</label>
<label>
Description
<textarea name="description" placeholder="Optional description" rows={2} />
</label>
<label>
Seed members (one per line, username:password)
<textarea name="users" placeholder="alice:password123" rows={4} />
</label>
<div className="actions">
<button type="submit" className="primary">
Create Access List
</button>
</div>
</form>
</section>
<style jsx>{`
.page {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
header p {
color: rgba(255, 255, 255, 0.6);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1.75rem;
}
.card {
background: rgba(16, 24, 38, 0.95);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.05);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.header {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.header input,
.header textarea {
width: 100%;
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 12, 20, 0.9);
color: #fff;
}
.entries ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.entries li {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(8, 12, 20, 0.9);
border-radius: 10px;
padding: 0.6rem 0.8rem;
}
.entries span {
font-weight: 500;
}
.empty {
color: rgba(255, 255, 255, 0.5);
}
.add-entry {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 0.6rem;
}
.add-entry input {
padding: 0.6rem 0.75rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 12, 20, 0.9);
color: #fff;
}
.primary {
padding: 0.6rem 1.3rem;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, #00c6ff 0%, #0072ff 100%);
color: #fff;
cursor: pointer;
}
.primary.small {
padding: 0.45rem 1rem;
}
.ghost {
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
}
.danger {
background: transparent;
border: 1px solid rgba(255, 91, 91, 0.6);
color: #ff5b5b;
padding: 0.5rem 1rem;
border-radius: 999px;
cursor: pointer;
align-self: flex-start;
}
.create {
background: rgba(16, 24, 38, 0.95);
border-radius: 16px;
padding: 1.75rem;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.form {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.form input,
.form textarea {
padding: 0.65rem 0.75rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(8, 12, 20, 0.9);
color: #fff;
}
.actions {
display: flex;
justify-content: flex-end;
}
`}</style>
</div>
);
return <AccessListsClient lists={lists} />;
}