Work on admin panel
This commit is contained in:
parent
6bff215d4f
commit
8d5baaa80f
|
@ -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
31
components/Nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
77
islands/Admin.tsx
Normal 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
24
islands/Clock.tsx
Normal 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>
|
||||
)
|
||||
}
|
48
models.ts
48
models.ts
|
@ -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 })
|
||||
|
|
|
@ -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
21
routes/admin.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue