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
|
<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
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 {
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
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 { 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 })
|
||||||
|
|
|
@ -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
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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue