From 43fb39331ce02607f14a122d6a9d6300937994fd Mon Sep 17 00:00:00 2001 From: solufa Date: Sat, 11 May 2024 04:41:40 +0900 Subject: [PATCH] feat: implement image viewer --- .../BasicHeader/BasicHeader.module.css | 4 +- client/pages/index.module.css | 75 ++++++++++++--- client/pages/index.page.tsx | 91 +++++++++++++------ 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/client/pages/@components/BasicHeader/BasicHeader.module.css b/client/pages/@components/BasicHeader/BasicHeader.module.css index 369aeb5..3b59c8b 100644 --- a/client/pages/@components/BasicHeader/BasicHeader.module.css +++ b/client/pages/@components/BasicHeader/BasicHeader.module.css @@ -1,6 +1,8 @@ .container { + position: sticky; + top: 0; height: 60px; - background: #fafafa; + background: #fff; border-bottom: 1px solid #eee; } diff --git a/client/pages/index.module.css b/client/pages/index.module.css index f887506..3ba247a 100644 --- a/client/pages/index.module.css +++ b/client/pages/index.module.css @@ -1,22 +1,73 @@ +.container { + min-height: 100vh; + padding: 40px 16px; + background: #f8f8f8; +} + +.main { + display: flex; + flex-direction: column; + gap: 32px; + width: 640px; + max-width: 100%; + margin: 0 auto; + margin-top: 32px; +} + +.card { + overflow: hidden; + background: #fff; + border-radius: 8px; + box-shadow: 2px 2px 12px #0004; +} + +@keyframes gradient { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + .title { + padding-top: 80px; font-size: 80px; - font-weight: bold; + font-weight: 900; + color: transparent; text-align: center; + background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + background-clip: text; + background-size: 200% 200%; + animation: gradient 10s ease infinite; +} + +.controls { + display: flex; + gap: 8px; + align-items: center; + padding: 12px 16px; } -.tasks { - width: 300px; - padding: 0; - margin: 20px auto 0; - text-align: left; - list-style-type: none; +.textInput { + width: 200px; + padding: 4px 8px; + border-radius: 4px; } -.tasks > li { - margin-top: 10px; - border-bottom: 1px solid #eee; +.btn { + padding: 4px 8px; + margin-left: auto; + cursor: pointer; + border-radius: 4px; } -.deleteBtn { - float: right; +.taskImage { + width: 100%; + vertical-align: bottom; } diff --git a/client/pages/index.page.tsx b/client/pages/index.page.tsx index e653d54..3d48034 100644 --- a/client/pages/index.page.tsx +++ b/client/pages/index.page.tsx @@ -3,7 +3,7 @@ import { Loading } from 'components/Loading/Loading'; import { useAtom } from 'jotai'; import { BasicHeader } from 'pages/@components/BasicHeader/BasicHeader'; import type { ChangeEvent, FormEvent } from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { apiClient } from 'utils/apiClient'; import { returnNull } from 'utils/returnNull'; import { userAtom } from '../atoms/user'; @@ -11,11 +11,17 @@ import styles from './index.module.css'; const Home = () => { const [user] = useAtom(userAtom); + const fileRef = useRef(null); const [tasks, setTasks] = useState(); const [label, setLabel] = useState(''); + const [image, setImage] = useState(); + const [previewImageUrl, setPreviewImageUrl] = useState(); const inputLabel = (e: ChangeEvent) => { setLabel(e.target.value); }; + const inputFile = (e: ChangeEvent) => { + setImage(e.target.files?.[0]); + }; const fetchTasks = async () => { const tasks = await apiClient.private.tasks.$get().catch(returnNull); @@ -23,10 +29,13 @@ const Home = () => { }; const createTask = async (e: FormEvent) => { e.preventDefault(); - if (!label) return; + if (!fileRef.current) return; - await apiClient.private.tasks.post({ body: { label } }).catch(returnNull); + await apiClient.private.tasks.post({ body: { label, image } }).catch(returnNull); setLabel(''); + setImage(undefined); + setPreviewImageUrl(undefined); + fileRef.current.value = ''; await fetchTasks(); }; const toggleDone = async (task: TaskEntity) => { @@ -47,35 +56,65 @@ const Home = () => { fetchTasks(); }, [user]); + useEffect(() => { + if (!image) return; + + const newUrl = URL.createObjectURL(image); + setPreviewImageUrl(newUrl); + return () => { + URL.revokeObjectURL(newUrl); + }; + }, [image]); + if (!tasks || !user) return ; return ( <> -
- Welcome to frourio! -
+
+
Welcome to frourio!
-
- - -
-
    - {tasks.map((task) => ( -
  • - - deleteTask(task)} - /> -
  • - ))} -
+
+
+
+ {previewImageUrl && } +
+ + + +
+
+
+ {tasks.map((task) => ( +
+ {task.image && ( + {task.label} + )} +
+ toggleDone(task)} /> + {task.label} + deleteTask(task)} + /> +
+
+ ))} +
+
); };