commit 510945b733af6afcf4cd1772593d6adbc84f3720 Author: [Quy Anh] «Elliot» Nguyen Date: Wed Jun 24 15:59:04 2026 +0200 --- diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..47346ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.env +.next +.git diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6b2a25c --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_BACKEND_URI= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00bba9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..39fcb1d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:24.4.1-alpine3.22 +WORKDIR /app +COPY . . +RUN npm ci +RUN npx next telemetry disable +RUN npm run build +RUN adduser -D appuser && chown -R appuser /app +USER appuser +CMD npm run start diff --git a/Providers.tsx b/Providers.tsx new file mode 100644 index 0000000..f0b2927 --- /dev/null +++ b/Providers.tsx @@ -0,0 +1,14 @@ +'use client' +import { apollo } from '@/lib' +import { ApolloProvider } from '@apollo/client/react' +import { ChakraProvider } from '@chakra-ui/react' + +export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + ) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..15f8e58 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Frontend for MemeSearch 🔎 diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..3ecfd0a --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,19 @@ +import { Providers } from '@/Providers' +import type { Metadata } from 'next' + + +export const metadata: Metadata = { + title: 'Meme Search Client' +} + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..0d4c5b7 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,78 @@ +'use client' +import { InputField, Wrapper } from '@/components' +import { SearchDocument } from '@/generated/graphql/graphql' +import { useQuery } from '@apollo/client/react' +import { Image } from '@chakra-ui/react' +import { Box, Button, Flex, Text } from '@chakra-ui/react' +import { Form, Formik } from 'formik' +// import { readFileSync } from 'fs' +import { useState } from 'react' + +const Home: React.FC = () => { + const [imageFile, setImageFile] = useState(null) + const { refetch, loading } = useQuery(SearchDocument, { skip: true }) + const [results, setResults] = useState<{ __typename?: string, image: string, text: string }[]>([]) + + return ( +
+ + { + if (imageFile) { + setResults([]) + const reader = new FileReader() + reader.readAsArrayBuffer(imageFile) + reader.onloadend = async () => { + const b64 = Buffer.from(reader.result as string, 'base64').toString('base64') + const result = await refetch({ image: b64, limit: 10 }) + setResults(result.data!.search) + setImageFile(null) + setValues({ image: '' }) + } + } + }} + > + {({ isSubmitting }) => ( +
+ { + setResults([]) + if (e.currentTarget.files) { + setImageFile(e.currentTarget.files[0]) + } + }} + /> + { + imageFile && + + Selected image: {imageFile.name} + + } + + + + + )} +
+ + { + results?.map(r => ( + + {r.text + {r.text} + + )) + } + +
+
+ ) +} +export default Home diff --git a/components/FormErrorMessage.tsx b/components/FormErrorMessage.tsx new file mode 100644 index 0000000..c1d8e59 --- /dev/null +++ b/components/FormErrorMessage.tsx @@ -0,0 +1,17 @@ +'use client' +import { CloseIcon } from '@chakra-ui/icons' +import { Flex, Text } from '@chakra-ui/react' + +interface Props { + message: string +} + +export const FormErrorMessage: React.FC = (props) => { + const { message } = props + return ( + + + {message} + + ) +} diff --git a/components/FormSuccessMessage.tsx b/components/FormSuccessMessage.tsx new file mode 100644 index 0000000..ad48c6d --- /dev/null +++ b/components/FormSuccessMessage.tsx @@ -0,0 +1,17 @@ +'use client' +import { CheckIcon } from '@chakra-ui/icons' +import { Flex, Text } from '@chakra-ui/react' + +interface Props { + message: string +} + +export const FormSuccessMessage: React.FC = (props) => { + const { message } = props + return ( + + + {message} + + ) +} diff --git a/components/InputField.tsx b/components/InputField.tsx new file mode 100644 index 0000000..3213318 --- /dev/null +++ b/components/InputField.tsx @@ -0,0 +1,18 @@ +'use client' +import { FormControl, FormErrorMessage, FormLabel, Input, InputProps } from '@chakra-ui/react' +import { useField } from 'formik' + +type Props = React.InputHTMLAttributes & InputProps & { name: string, label: string, isTextArea?: boolean } + +export const InputField: React.FC = ({size: _, isTextArea=false, ...props}) => { + const [field, { error }] = useField(props) + const { label, placeholder } = props + + return ( + + {label} + + {error ? {error} : null} + + ) +} diff --git a/components/RefetchButton.tsx b/components/RefetchButton.tsx new file mode 100644 index 0000000..f3384af --- /dev/null +++ b/components/RefetchButton.tsx @@ -0,0 +1,10 @@ +'use client' +import { Button, PropsOf } from '@chakra-ui/react' +import { useRouter } from 'next/navigation' + +export const RefetchButton: React.FC> = (props) => { + const router = useRouter() + return ( + + ) +} diff --git a/components/TextareaField.tsx b/components/TextareaField.tsx new file mode 100644 index 0000000..8ac4425 --- /dev/null +++ b/components/TextareaField.tsx @@ -0,0 +1,36 @@ +'use client' +import { FormControl, FormErrorMessage, FormLabel, Textarea } from '@chakra-ui/react' +import autosize from 'autosize' +import { useField } from 'formik' +import { useRef, useEffect } from 'react' + +type Props = React.TextareaHTMLAttributes & { name: string, label: string } + +export const TextareaField: React.FC = (props) => { + const [field, { error }] = useField(props) + const { label, placeholder } = props + + // https://github.com/chakra-ui/chakra-ui/issues/670 + const ref: any = useRef(null) + useEffect(() => { + const current = ref.current + autosize(current) + return () => { + autosize.destroy(current) + } + }, []) + + return ( + + {label} +