diff --git a/todo_app/src/pages/index.module.css b/todo_app/src/pages/index.module.css index 940814847db516164d402626588bf3d1e2a02331..25a8cfdd157f3a6029ac82bc801766687022b8c2 100644 --- a/todo_app/src/pages/index.module.css +++ b/todo_app/src/pages/index.module.css @@ -177,19 +177,78 @@ } .todoClass { + /* border: 0; background:none; color: hsl(280 100% 70%); + */ } .todoClass:hover { color: hsl(280 100% 90%); text-decoration: line-through; + filter:opacity(40%); } .doneClass { text-decoration: line-through; + filter:opacity(40%); } .doneClass:hover { text-decoration: none; + filter:none; + /* + text-decoration: none; + */ +} +.addImg { + background:none; + border:0; + outline:none; + font-size:3rem; +} +.imgprev { + max-width: 100px; + max-height: 100px; + margin:0 auto; +} + +.newTodo { + outline: none; +} + +.addButton { + outline: none; + border: none; + background: none; + font-size: 4rem; + text-align: right; + vertical-align: bottom; + color: violet; +} + +.createCard { + position:relative; + min-height: 170px; + min-width: 300px; +} + +.createCard:before { + position:absolute; + color:violet; + filter:opacity(10%); + content:"+"; + font-size:10rem; + margin-top:-30px; + margin-left: 50%; + text-align:center; +} + +.showAll { + font-size:3rem; + margin-top:-2rem; +} + +.doneToggle{ + text-align:right; } diff --git a/todo_app/src/pages/todo.tsx b/todo_app/src/pages/todo.tsx index c7148b3e31f5eb9db465805e54fe7fb27f710102..820cf2bbb1864e10c328f5f8ea76f579f77b2323 100644 --- a/todo_app/src/pages/todo.tsx +++ b/todo_app/src/pages/todo.tsx @@ -1,16 +1,34 @@ import Head from "next/head"; -import Link from "next/link"; +// import Link from "next/link"; import { api } from "~/utils/api"; import styles from "./index.module.css"; -import React, { useState } from 'react' +import React, { useRef, useEffect, useState } from 'react' import { useFilePicker } from 'use-file-picker'; export default function Home() { - const { openFilePicker, filesContent, loading } = useFilePicker({ + let imgTodo: number|undefined = undefined + + const imgHeader = 'data:image/jpeg;base64,' + + const addImg = api.todo.addImg.useMutation({ + onSuccess(_input) {invalidate()} + }) + + const { openFilePicker, filesContent, loading, errors } = useFilePicker({ + readAs: 'DataURL', + //accept: 'image/*', accept: '.jpg', + multiple: false, + onFilesSuccessfullySelected: ({ filesContent }) => { + let base64 = filesContent[0]?.content ?? '' + if(!base64.startsWith(imgHeader) || imgTodo === undefined) + return + base64 = base64.substring(imgHeader.length) + addImg.mutate({id: imgTodo, img: base64}) + }, }) const utils = api.useContext() @@ -21,22 +39,44 @@ export default function Home() { void utils.todo.query.invalidate() } + const addImage = (e: React.SyntheticEvent<EventTarget>) => { + if (!(e.target instanceof HTMLElement)) + return + if(!e.target.dataset.id) + return + imgTodo = +e.target.dataset.id + e.stopPropagation() + e.preventDefault() + openFilePicker() + } + + const rmImage = (e: React.SyntheticEvent<EventTarget>) => { + if (!(e.target instanceof HTMLElement)) + return + if(!e.target.dataset.id) + return + addImg.mutate({id: +e.target.dataset.id, img: ''}) + e.stopPropagation() + e.preventDefault() + } + const toggleTodo = api.todo.toggleDone.useMutation({ - onSuccess(input) {invalidate()} + onSuccess(_input) {invalidate()} }) const createTodo = api.todo.create.useMutation({ - onSuccess(input) { + onSuccess(_input) { const txt = (document.getElementById('create-todo-text') as HTMLInputElement) if(txt !== undefined) { txt.value = '' + txt.focus() } invalidate() } }) - const toggle = (e: React.SyntheticEvent<EventTarget>) => { - if (!(e.target instanceof HTMLButtonElement)) + const toggle = (e: React.SyntheticEvent<EventTarget>) => { + if (!(e.target instanceof HTMLElement)) return if(!e.target.dataset.id) return @@ -44,7 +84,12 @@ export default function Home() { } const create = (e: React.SyntheticEvent<EventTarget>) => { - createTodo.mutate({'text': (document.getElementById('create-todo-text') as HTMLInputElement)?.value ?? '' }) + const elm = document.getElementById('create-todo-text') as HTMLElement + if(elm === undefined) + return + createTodo.mutate({'text': elm.innerText ?? '' }) + elm.innerText = '' + elm.focus() } const toggleShowAll = (e: React.SyntheticEvent<EventTarget>) => { @@ -52,10 +97,16 @@ export default function Home() { invalidate() } + + const focusElement = useRef(null) + useEffect(() => { + if (focusElement.current) + (focusElement.current as HTMLElement).focus() + }, []); + if (loading) return <div>Loading...</div> - return ( <> <Head> @@ -67,15 +118,33 @@ export default function Home() { tRPC <span className={styles.pinkSpan}>Todo</span> App </h1> <div className={styles.container}> - <div className={styles.card}> - <div> - <input type="text" id="create-todo-text"></input> - <button onClick={create}>create</button> + <center> + <div className={styles.showAll} onClick={toggleShowAll}> + {showAll ? '⌛' : '⏳'} + </div> + </center> + <div className={styles.cardRow}> + + <div + className={styles.card + ' ' + styles.createCard} + onClick={create} + > + <h3 ref={focusElement} contentEditable={true} className={styles.cardTitle + ' ' + styles.newTodo} id="create-todo-text"></h3> + </div> + + {queryTodos.data?.map((todo, index) => + <div className={styles.card} + key={index} + > + <h3 onClick={toggle} data-id={todo.id} + className={`${styles.cardTitle} ${todo.done ? styles.doneClass : styles.todoClass}`} + >{todo.text}</h3> + {todo.img + ? <img onClick={rmImage} data-id={todo.id} src={imgHeader + ' ' + todo.img} className={styles.imgprev}></img> + : <button className={styles.addImg} data-id={todo.id} onClick={addImage}>🖼️</button> + } </div> - {queryTodos.data?.map((todo, index) => - <button key={index} className={todo.done ? styles.doneClass + ' ' + styles.todoClass : styles.todoClass} onClick={toggle} data-id={todo.id}>{todo.text}</button>) - ?? ''} - <label><input id="showdone" checked={showAll} onClick={toggleShowAll} type="checkbox"></input> show done</label> + ) ?? ''} </div> </div> </main> diff --git a/todo_app/src/server/api/routers/todo.ts b/todo_app/src/server/api/routers/todo.ts index f893b840fa4fc4875f27a88d5b8ce429f02f9326..3a3e00da030657810df3f47026ead8a0fa28b3c6 100644 --- a/todo_app/src/server/api/routers/todo.ts +++ b/todo_app/src/server/api/routers/todo.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc" -type Todo = {id: number, text: string, done: boolean} +type Todo = {id: number, text: string, done: boolean, imgid?: number, img?: string} const todos: Todo[] = [ { id:1, text: 'create trpc todo app', done: true }, { id:2, text: 'explain trpc to peers', done: false }, @@ -13,15 +13,27 @@ export const todoRouter = createTRPCRouter({ create: publicProcedure .input(z.object({ text: z.string().min(1) })) .mutation(async ({ input }) => { - const todo = { id: todos.length + 1, text: input.text, done: false } + const todo = { id: todos.length + 1, text: input.text, done: false, imgid: undefined, img: undefined } todos.push(todo) return todo }), + addImg: publicProcedure + .input(z.object({ id: z.number().int(), img: z.string() })) + .mutation(async ({ input }) => { + const found = todos.filter((t) => t.id === input.id) + console.log(input) + if (found.length != 1 || found[0] === undefined) + return + // should go roundtrip via grpc svc first + // found[0].imgid = ??? + found[0].img = input.img + console.log(todos) + }), + query: publicProcedure .input(z.boolean()) .query(({input}) => { - console.log('all', input) return todos.filter((t) => input || !t.done) }),