Skip to content

Commit

Permalink
Yozumina frontend (#26)
Browse files Browse the repository at this point in the history
add: image preview before upload, upload thumbnail in chirp bubble
add dependencies: React-redux, @reduxjs/toolkit
edit: chirp component into bottom component

---------

Co-authored-by: pbzweihander <pbzweihander@protonmail.com>
  • Loading branch information
Squarecat-meow and pbzweihander authored Jul 27, 2024
1 parent 78753f9 commit af36c0e
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 49 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/docker-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: docker/setup-qemu-action@v2
if: github.event_name != 'pull_request'
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
- uses: docker/setup-buildx-action@v2
- uses: docker/metadata-action@v4
id: meta
Expand All @@ -31,7 +31,7 @@ jobs:
- uses: docker/build-push-action@v4
with:
context: .
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
platforms: ${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
cache-to: type=gha,scope=docker
cache-from: type=gha,scope=docker,mode=max
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"dependencies": {
"@heroicons/react": "^2.0.18",
"jotai": "^2.8.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/components/Images.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from "react";
import z from "zod";

import { File } from "../dto";

interface propsType {
files: z.infer<typeof File>[];
}

const Images = ({ files }: propsType) => {
const [clicked, setClicked] = useState(false);
const [url, setUrl] = useState<string>("");

const handleImageClick = (url: string) => {
setClicked(!clicked);
setUrl(url);
};

return (
<div>
{files &&
files.map((file) => (
<img
key={file.url}
src={file.url}
className="max-w-[24rem] cursor-pointer"
onClick={() => handleImageClick(file.url)}
/>
))}
{clicked && (
<div
className="fixed left-0 top-0 z-10 flex items-center justify-center bg-slate-500/40"
onClick={() => setClicked(!clicked)}
>
<img className="h-screen w-screen object-scale-down" src={url} />
</div>
)}
</div>
);
};

export default Images;
100 changes: 100 additions & 0 deletions frontend/src/components/NewChirp/BottomNewChirp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { TrashIcon } from "@heroicons/react/24/outline";
import { useAtomValue, useSetAtom } from "jotai";
import { useEffect } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import z from "zod";

import { CreatePost } from "../../dto";
import { usePostNoteMutation } from "../../queries/note";
import { pictureUrl } from "../../states/states";
import BottomUpload from "./BottomUpload";

export default function BottomNewChirp() {
const { register, handleSubmit, setValue, reset } =
useForm<z.infer<typeof CreatePost>>();
const {
mutate: postNote,
isLoading,
error,
} = usePostNoteMutation(() => {
reset();
});
const pictureUrlArr = useAtomValue(pictureUrl);
const deleteUrl = useSetAtom(pictureUrl);

const onSubmit: SubmitHandler<z.infer<typeof CreatePost>> = (data) => {
postNote(data);
deletePicture();
};

const deletePicture = () => {
deleteUrl(() => []);
};

useEffect(() => {
const ulid: string[] = pictureUrlArr.map((el: string) =>
el.substring(el.length - 26, el.length),
);
setValue("files", ulid);
}, [pictureUrlArr, setValue]);

return (
<>
<form
className="chat chat-end absolute bottom-4 right-4"
onSubmit={handleSubmit(onSubmit)}
>
<input type="hidden" value="public" {...register("visibility")} />
<div className="chat-bubble chat-bubble-primary">
<div className="flex items-center">
<BottomUpload />
<div>
{pictureUrlArr.length > 0 && (
<div>
{pictureUrlArr.map((value, i) => (
<div key={i}>
<input
type="hidden"
value={pictureUrlArr}
{...register("files")}
/>
<img
src={value}
alt="pictureurl"
className="w-48 rounded-lg"
/>
<button
type="button"
className="btn btn-circle btn-error btn-sm absolute right-4 top-4"
onClick={() => deletePicture()}
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
))}
</div>
)}

<input
type="text"
className="input w-full bg-transparent"
placeholder="Write something..."
required
{...register("text")}
/>
</div>
</div>
</div>
<div className="chat-footer">
<input
type="submit"
className="btn btn-primary btn-sm mt-2"
value="Chirp!"
disabled={isLoading}
/>
</div>
{error && <div className="mt-5 text-error">{error.message}</div>}
</form>
</>
);
}
59 changes: 59 additions & 0 deletions frontend/src/components/NewChirp/BottomUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ArrowUpTrayIcon } from "@heroicons/react/24/outline";
import { useSetAtom } from "jotai";
import { Fragment, useRef } from "react";

import { useLocalFiles } from "../../queries/file";
import { pictureUrl } from "../../states/states";

export default function BottomUpload() {
const modalRef = useRef<HTMLDialogElement>(null);
const setUrl = useSetAtom(pictureUrl);

const { data } = useLocalFiles();

const handlePictureClick = (url: string) => {
setUrl((el) => [...el, url]);

modalRef?.current?.close();
};

return (
<>
<div
onClick={() => modalRef?.current?.showModal()}
className="mr-4 rounded-lg p-1 hover:bg-slate-50 hover:bg-opacity-10 active:bg-slate-700"
>
<ArrowUpTrayIcon width={24} height={24} />
</div>
<dialog ref={modalRef} className="modal">
<div className="modal-box max-w-screen-xl">
{(data?.pages ?? []).map((page, i) => (
<div key={i} className="flex">
<Fragment>
{page.length !== 0 ? (
<div className="grid grid-cols-3 gap-4">
{page.map((file) => (
<div key={file.id} className="h-48 w-48">
<img
src={file.url}
alt={file.alt ?? undefined}
className="cursor-pointer rounded-lg border border-solid hover:shadow-lg"
onClick={() => handlePictureClick(file.url)}
/>
</div>
))}
</div>
) : (
<span>no items</span>
)}
</Fragment>
</div>
))}
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
</>
);
}
26 changes: 23 additions & 3 deletions frontend/src/components/RightNavUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PlusIcon } from "@heroicons/react/24/outline";
import { useRef } from "react";
import { PlusIcon, PhotoIcon } from "@heroicons/react/24/outline";
import { useEffect, useRef, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";

import { useLocalFileUploadMutation } from "../queries/file";
Expand All @@ -13,7 +13,8 @@ interface UploadForm {

export default function RightNavUpload() {
const modalRef = useRef<HTMLDialogElement>(null);
const { register, handleSubmit, reset } = useForm<UploadForm>();
const [imagePreview, setImagePreview] = useState<string>("");
const { register, handleSubmit, reset, watch } = useForm<UploadForm>();
const {
mutate: upload,
isLoading,
Expand All @@ -22,6 +23,7 @@ export default function RightNavUpload() {
reset();
modalRef.current?.close();
});
const image = watch("files");

const onSubmit: SubmitHandler<UploadForm> = (data) => {
if (!data.files) {
Expand All @@ -32,6 +34,15 @@ export default function RightNavUpload() {
upload({ file, mediaType: file.type, alt: data.alt });
};

useEffect(() => {
if (image && image.length > 0) {
const file = image[0];
setImagePreview(URL.createObjectURL(file));
} else {
setImagePreview("");
}
}, [image]);

return (
<>
<button
Expand All @@ -53,6 +64,15 @@ export default function RightNavUpload() {
required
{...register("files")}
/>
<div className="mb-4 flex size-full items-center justify-center border border-solid">
{imagePreview !== "" ? (
<img src={imagePreview} alt="uploaded image" />
) : (
<div className="flex h-[24rem] w-[24rem] items-center justify-center">
<PhotoIcon width={48} height={48} stroke="#e6e6e6" />
</div>
)}
</div>
<textarea
className="textarea textarea-bordered mb-4 w-full"
placeholder="Alt text..."
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Provider as JotaiProvider } from "jotai";
import React from "react";
import ReactDOM from "react-dom/client";

import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
<JotaiProvider>
<React.StrictMode>
<App />
</React.StrictMode>
</JotaiProvider>,
);
49 changes: 8 additions & 41 deletions frontend/src/pages/login/LogInIndex.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import { PlusIcon } from "@heroicons/react/24/outline";
import { Fragment } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import z from "zod";

import { CreatePost } from "../../dto";
import { useNotes, usePostNoteMutation } from "../../queries/note";
import Images from "../../components/Images";
import BottomNewChirp from "../../components/NewChirp/BottomNewChirp";
import { useNotes } from "../../queries/note";

export function LogInIndexPage() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useNotes();
const { register, handleSubmit, reset } =
useForm<z.infer<typeof CreatePost>>();
const {
mutate: postNote,
isLoading,
error,
} = usePostNoteMutation(() => {
reset();
});

const onSubmit: SubmitHandler<z.infer<typeof CreatePost>> = (data) => {
postNote(data);
};

return (
<div className="relative flex h-full w-full">
Expand All @@ -46,7 +32,10 @@ export function LogInIndexPage() {
)}
</div>
)}
<div className="chat-bubble">{note.text}</div>
<div className="chat-bubble">
<Images files={note.files} />
{note.text}
</div>
</div>
))}
</Fragment>
Expand All @@ -67,29 +56,7 @@ export function LogInIndexPage() {
)}
</div>
</div>
<form
className="chat chat-end absolute bottom-4 right-4"
onSubmit={handleSubmit(onSubmit)}
>
<input type="hidden" value="public" {...register("visibility")} />
<div className="chat-bubble chat-bubble-primary">
<textarea
className="textarea w-full text-base-content"
placeholder="Jot something..."
required
{...register("text")}
/>
</div>
<div className="chat-footer">
<input
type="submit"
className="btn btn-primary btn-sm mt-2"
value="Chirp!"
disabled={isLoading}
/>
</div>
{error && <div className="mt-5 text-error">{error.message}</div>}
</form>
<BottomNewChirp />
</div>
);
}
3 changes: 3 additions & 0 deletions frontend/src/states/states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atom } from "jotai";

export const pictureUrl = atom<string[]>([]);
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1553,6 +1553,11 @@ jiti@^1.21.0:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==

jotai@^2.8.3:
version "2.8.3"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.8.3.tgz#21b50c89c9ee2d24e694158b925bade1f38641d7"
integrity sha512-pR4plVvdbzB6zyt7VLLHPMAkcRSKhRIvZKd+qkifQLa3CEziEo1uwZjePj4acTmQrboiISBlYSdCz3gWcr1Nkg==

js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
Expand Down

0 comments on commit af36c0e

Please sign in to comment.