Work on admin panel

This commit is contained in:
Daniel Flanagan 2024-01-07 02:24:36 -06:00
parent 6bff215d4f
commit 8d5baaa80f
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
12 changed files with 262 additions and 38 deletions

View file

@ -6,7 +6,7 @@ export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
<button <button
{...props} {...props}
disabled={!IS_BROWSER || props.disabled} disabled={!IS_BROWSER || props.disabled}
class='px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors' class='px-2 py-1 bg-gray-500/20 rounded hover:bg-gray-500/25 cursor-pointer transition-colors'
/> />
) )
} }

31
components/Nav.tsx Normal file
View file

@ -0,0 +1,31 @@
// import { JSX } from 'preact'
// import { IS_BROWSER } from '$fresh/runtime.ts'
import { Clock } from '@homeman/islands/Clock.tsx'
export function Nav(/* props: {} */) {
return (
<nav class='bg-stone-200 dark:bg-stone-800 flex justify-items-start items-center'>
<button class='p-4 hover:bg-stone-500/20'>
<svg
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
stroke-width='1.5'
stroke='currentColor'
class='w-6 h-6'
>
<path
stroke-linecap='round'
stroke-linejoin='round'
d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'
/>
</svg>
</button>
<a class='p-4 hover:bg-stone-500/20' href='/'>
Flanagan Family
</a>
<Clock class='ml-auto pr-4 text-2xl' />
</nav>
)
}

View file

@ -1,13 +1,39 @@
import { Todo, User } from '$models' import { Todo, UserWithTodos } from '@homeman/models.ts'
export interface Props { export interface Props {
user: User user: UserWithTodos
} }
export function TodoList({ user }: Props) { export function TodoList({ user: { avatarUrl, assignedTodos, name } }: Props) {
const todoItem = ({ description }: Todo) => (
<li class='bg-white dark:bg-black drop-shadow-xl'>{description}</li>
)
return ( return (
<div> <div class='p-2 w-1/4 min-w-[10rem] relative flex flex-col grow-0'>
<h1>Todo List for {user}</h1> {avatarUrl != null
? (
<img
class='rounded-full w-full mb-2'
src={avatarUrl}
title={`${name}'s avatar`}
/>
)
: null}
<span class='text-sky-800 dark:text-sky-200 font-semibold text-center'>
{name}
</span>
<button class='mt-4 mb-4 text-left w-full px-2 py-1 rounded-lg border-[1px] text-stone-500 border-stone-500 opacity-50 hover:opacity-100'>
+ New
</button>
<ul class=''>
{assignedTodos.length < 1
? (
<li class='p-4 rounded-lg text-center drop-shadow-xl bg-white dark:bg-stone-900'>
All clear! 🎉
</li>
)
: assignedTodos.map(todoItem)}
</ul>
</div> </div>
) )
} }

View file

@ -29,8 +29,8 @@
"tailwindcss": "npm:tailwindcss@3.3.5", "tailwindcss": "npm:tailwindcss@3.3.5",
"tailwindcss/": "npm:/tailwindcss@3.3.5/", "tailwindcss/": "npm:/tailwindcss@3.3.5/",
"tailwindcss/plugin": "npm:/tailwindcss@3.3.5/plugin.js", "tailwindcss/plugin": "npm:/tailwindcss@3.3.5/plugin.js",
"$models": "./models.ts", "$std/": "https://deno.land/std@0.208.0/",
"$std/": "https://deno.land/std@0.208.0/" "@homeman/": "./"
}, },
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",

View file

@ -4,9 +4,12 @@
import * as $_404 from './routes/_404.tsx' import * as $_404 from './routes/_404.tsx'
import * as $_app from './routes/_app.tsx' import * as $_app from './routes/_app.tsx'
import * as $admin from './routes/admin.tsx'
import * as $api_joke from './routes/api/joke.ts' import * as $api_joke from './routes/api/joke.ts'
import * as $greet_name_ from './routes/greet/[name].tsx' import * as $greet_name_ from './routes/greet/[name].tsx'
import * as $index from './routes/index.tsx' import * as $index from './routes/index.tsx'
import * as $Admin from './islands/Admin.tsx'
import * as $Clock from './islands/Clock.tsx'
import * as $Counter from './islands/Counter.tsx' import * as $Counter from './islands/Counter.tsx'
import { type Manifest } from '$fresh/server.ts' import { type Manifest } from '$fresh/server.ts'
@ -14,11 +17,14 @@ const manifest = {
routes: { routes: {
'./routes/_404.tsx': $_404, './routes/_404.tsx': $_404,
'./routes/_app.tsx': $_app, './routes/_app.tsx': $_app,
'./routes/admin.tsx': $admin,
'./routes/api/joke.ts': $api_joke, './routes/api/joke.ts': $api_joke,
'./routes/greet/[name].tsx': $greet_name_, './routes/greet/[name].tsx': $greet_name_,
'./routes/index.tsx': $index, './routes/index.tsx': $index,
}, },
islands: { islands: {
'./islands/Admin.tsx': $Admin,
'./islands/Clock.tsx': $Clock,
'./islands/Counter.tsx': $Counter, './islands/Counter.tsx': $Counter,
}, },
baseUrl: import.meta.url, baseUrl: import.meta.url,

77
islands/Admin.tsx Normal file
View file

@ -0,0 +1,77 @@
import { createRef } from 'preact'
import { Todo, User } from '@homeman/models.ts'
import { Button } from '@homeman/components/Button.tsx'
export interface Props {
users: User[]
todos: Todo[]
}
export function Admin({ users, todos }: Props) {
const addUserDialog = createRef<HTMLDialogElement>()
const usersById: Record<string, User> = {}
for (const u of users) {
usersById[u.id] = u
}
return (
<main class='flex flex-col'>
<dialog ref={addUserDialog}>sup</dialog>
<header class='flex items-center border-b-2 border-stone-500/20 '>
<h1 class='p-5 text-2xl'>
Users ({users.length})
</h1>
<Button onClick={() => addUserDialog.current?.showModal()}>
Add User
</Button>
</header>
<table class='border-separate [border-spacing:1.25rem] text-left'>
<thead>
<tr>
<th>Name</th>
<th>Avatar</th>
<th>Color</th>
</tr>
</thead>
<tbody>
{users.map(({ name, avatarUrl, color }) => (
<tr>
<td>{name}</td>
<td>
{avatarUrl == null ? 'None' : <img class='' src={avatarUrl} />}
</td>
<td>
#{color}
</td>
</tr>
))}
</tbody>
</table>
<header class='flex items-center border-b-2 border-stone-500/20 '>
<h1 class='p-5 text-2xl'>
Todos ({users.length})
</h1>
<Button>+</Button>
</header>
<table class='border-separate [border-spacing:1.25rem] text-left'>
<thead>
<tr>
<th>Description</th>
<th>Assignee</th>
</tr>
</thead>
<tbody>
{todos.map(({ description, assigneeUserId }) => (
<tr>
<td>{description}</td>
<td>
{assigneeUserId == null
? 'Unassigned'
: usersById[assigneeUserId]?.name}
</td>
</tr>
))}
</tbody>
</table>
</main>
)
}

24
islands/Clock.tsx Normal file
View file

@ -0,0 +1,24 @@
import { JSX } from 'preact'
import { signal } from '@preact/signals'
import { IS_BROWSER } from '$fresh/runtime.ts'
const count = signal(new Date())
if (IS_BROWSER) {
setInterval(() => count.value = new Date(), 1000)
}
export function Clock(props: JSX.HTMLAttributes<HTMLSpanElement>) {
const dt = count.value
return (
<span {...props} title={dt.toISOString()}>
{dt.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
// second: '2-digit',
})
.toLowerCase().replaceAll(' ', '')}
</span>
)
}

View file

@ -1,17 +1,31 @@
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts' import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
import { createPentagon } from 'https://deno.land/x/pentagon@v0.1.5/mod.ts' import { createPentagon } from 'https://deno.land/x/pentagon@v0.1.5/mod.ts'
import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts' // import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts'
const kv = await Deno.openKv() const kv = await Deno.openKv()
// const todos = kv.list({ prefix: ['todos'] })
// const deleteme = []
// for await (const u of todos) {
// deleteme.push(u.key)
// console.log(u)
// }
// for (const d of deleteme) {
// await kv.delete(d)
// }
const User = z.object({ const User = z.object({
id: z.string().ulid().describe('primary'), id: z.string().ulid().describe('primary'),
createdAt: z.date(), createdAt: z.date(),
name: z.string(), name: z.string(),
avatarUrl: z.string().nullable(), avatarUrl: z.string().nullable(),
color: z.string().nullable(),
}) })
export type User = z.infer<typeof User> export type User = z.infer<typeof User>
export type UserWithTodos = User & {
assignedTodos: Todo[]
}
const Todo = z.object({ const Todo = z.object({
id: z.string().ulid().describe('primary'), id: z.string().ulid().describe('primary'),
@ -40,20 +54,20 @@ export const db = createPentagon(kv, {
}, },
}) })
const daddy: User = { // const daddy: User = {
id: ulid(), // id: ulid(),
createdAt: new Date(), // createdAt: new Date(),
name: 'Daddy', // name: 'Daddy',
avatarUrl: null, // avatarUrl: null,
} // }
const todo: Todo = { // const todo: Todo = {
emoji: null, // emoji: null,
id: ulid(), // id: ulid(),
createdAt: new Date(), // createdAt: new Date(),
description: 'Test Todo', // description: 'Test Todo',
doneAt: null, // doneAt: null,
assigneeUserId: daddy.id, // assigneeUserId: daddy.id,
} // }
db.todos.create({ data: todo }) // db.todos.create({ data: todo })
db.users.create({ data: daddy }) // db.users.create({ data: daddy })

View file

@ -1,4 +1,6 @@
import { type PageProps } from '$fresh/server.ts' import { type PageProps } from '$fresh/server.ts'
import { Nav } from '@homeman/components/Nav.tsx'
export default function App({ Component }: PageProps) { export default function App({ Component }: PageProps) {
return ( return (
<html> <html>
@ -8,8 +10,8 @@ export default function App({ Component }: PageProps) {
<title>homeman-deno</title> <title>homeman-deno</title>
<link rel='stylesheet' href='/styles.css' /> <link rel='stylesheet' href='/styles.css' />
</head> </head>
<body class='dark:bg-zinc-950 dark:text-zinc-100'> <body class='bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100 min-h-lvh'>
meow <Nav />
<Component /> <Component />
</body> </body>
</html> </html>

21
routes/admin.tsx Normal file
View file

@ -0,0 +1,21 @@
import { Handlers, PageProps } from '$fresh/server.ts'
import { db } from '@homeman/models.ts'
import { Admin, Props } from '@homeman/islands/Admin.tsx'
export const handler: Handlers = {
async GET(_req, ctx) {
const users = await db.users.findMany({})
const todos = await db.todos.findMany({})
return ctx.render({ users, todos })
},
}
export default function Home(
{ data: props }: PageProps<Props>,
) {
return (
<main class='flex flex-col'>
<Admin {...props} />
</main>
)
}

View file

@ -1,10 +1,17 @@
import { Handlers, PageProps } from '$fresh/server.ts' import { Handlers, PageProps } from '$fresh/server.ts'
import { useSignal } from '@preact/signals' import { db, Todo, User, UserWithTodos } from '@homeman/models.ts'
import { db, Todo, User } from '$models' import { TodoList } from '@homeman/components/TodoList.tsx'
import { TodoList } from '../components/TodoList.tsx'
const unassignedUserPlaceholder: User = {
id: '',
createdAt: new Date(),
name: 'Shared',
avatarUrl: 'http://placekitten.com/512/512',
color: '888888',
}
interface Data { interface Data {
users: User[] users: UserWithTodos[]
unassignedTodos: Todo[] unassignedTodos: Todo[]
} }
@ -21,12 +28,25 @@ export const handler: Handlers = {
export default function Home( export default function Home(
{ data: { users, unassignedTodos } }: PageProps<Data>, { data: { users, unassignedTodos } }: PageProps<Data>,
) { ) {
const count = useSignal(3) const unassignedUser: UserWithTodos = {
...unassignedUserPlaceholder,
assignedTodos: unassignedTodos,
}
return ( return (
<div class='px-4 py-8 mx-auto bg-emerald-950'> <main class='flex flex-col'>
<div class='max-w-screen-md mx-auto flex flex-col items-center justify-center'> <h1 class='p-5 border-b-2 border-stone-500/20 text-2xl'>
{users.map((u) => <TodoList user={u} />)} Todos
</div> </h1>
</div> <ul class='p-4 relative'>
<li>
<TodoList user={unassignedUser} />
</li>
{users.map((u) => (
<li>
<TodoList user={u} />
</li>
))}
</ul>
</main>
) )
} }

View file

@ -1,3 +1,6 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
body {
}