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
{...props}
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 {
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 (
<div>
<h1>Todo List for {user}</h1>
<div class='p-2 w-1/4 min-w-[10rem] relative flex flex-col grow-0'>
{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>
)
}

View file

@ -29,8 +29,8 @@
"tailwindcss": "npm:tailwindcss@3.3.5",
"tailwindcss/": "npm:/tailwindcss@3.3.5/",
"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": {
"jsx": "react-jsx",

View file

@ -4,9 +4,12 @@
import * as $_404 from './routes/_404.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 $greet_name_ from './routes/greet/[name].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 { type Manifest } from '$fresh/server.ts'
@ -14,11 +17,14 @@ const manifest = {
routes: {
'./routes/_404.tsx': $_404,
'./routes/_app.tsx': $_app,
'./routes/admin.tsx': $admin,
'./routes/api/joke.ts': $api_joke,
'./routes/greet/[name].tsx': $greet_name_,
'./routes/index.tsx': $index,
},
islands: {
'./islands/Admin.tsx': $Admin,
'./islands/Clock.tsx': $Clock,
'./islands/Counter.tsx': $Counter,
},
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 { 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 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({
id: z.string().ulid().describe('primary'),
createdAt: z.date(),
name: z.string(),
avatarUrl: z.string().nullable(),
color: z.string().nullable(),
})
export type User = z.infer<typeof User>
export type UserWithTodos = User & {
assignedTodos: Todo[]
}
const Todo = z.object({
id: z.string().ulid().describe('primary'),
@ -40,20 +54,20 @@ export const db = createPentagon(kv, {
},
})
const daddy: User = {
id: ulid(),
createdAt: new Date(),
name: 'Daddy',
avatarUrl: null,
}
const todo: Todo = {
emoji: null,
id: ulid(),
createdAt: new Date(),
description: 'Test Todo',
doneAt: null,
assigneeUserId: daddy.id,
}
// const daddy: User = {
// id: ulid(),
// createdAt: new Date(),
// name: 'Daddy',
// avatarUrl: null,
// }
// const todo: Todo = {
// emoji: null,
// id: ulid(),
// createdAt: new Date(),
// description: 'Test Todo',
// doneAt: null,
// assigneeUserId: daddy.id,
// }
db.todos.create({ data: todo })
db.users.create({ data: daddy })
// db.todos.create({ data: todo })
// db.users.create({ data: daddy })

View file

@ -1,4 +1,6 @@
import { type PageProps } from '$fresh/server.ts'
import { Nav } from '@homeman/components/Nav.tsx'
export default function App({ Component }: PageProps) {
return (
<html>
@ -8,8 +10,8 @@ export default function App({ Component }: PageProps) {
<title>homeman-deno</title>
<link rel='stylesheet' href='/styles.css' />
</head>
<body class='dark:bg-zinc-950 dark:text-zinc-100'>
meow
<body class='bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100 min-h-lvh'>
<Nav />
<Component />
</body>
</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 { useSignal } from '@preact/signals'
import { db, Todo, User } from '$models'
import { TodoList } from '../components/TodoList.tsx'
import { db, Todo, User, UserWithTodos } from '@homeman/models.ts'
import { TodoList } from '@homeman/components/TodoList.tsx'
const unassignedUserPlaceholder: User = {
id: '',
createdAt: new Date(),
name: 'Shared',
avatarUrl: 'http://placekitten.com/512/512',
color: '888888',
}
interface Data {
users: User[]
users: UserWithTodos[]
unassignedTodos: Todo[]
}
@ -21,12 +28,25 @@ export const handler: Handlers = {
export default function Home(
{ data: { users, unassignedTodos } }: PageProps<Data>,
) {
const count = useSignal(3)
const unassignedUser: UserWithTodos = {
...unassignedUserPlaceholder,
assignedTodos: unassignedTodos,
}
return (
<div class='px-4 py-8 mx-auto bg-emerald-950'>
<div class='max-w-screen-md mx-auto flex flex-col items-center justify-center'>
{users.map((u) => <TodoList user={u} />)}
</div>
</div>
<main class='flex flex-col'>
<h1 class='p-5 border-b-2 border-stone-500/20 text-2xl'>
Todos
</h1>
<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 components;
@tailwind utilities;
body {
}