From eb1a553fbf9ae9dc183b6c8a8d6ce43b3935460d Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 16 Feb 2024 00:16:19 +0400 Subject: [PATCH 01/25] chore: add unsaved code --- src/app/api/messages/chat/route.ts | 28 +++ .../elements/anchor/components/anchor.tsx | 16 ++ src/components/elements/anchor/index.ts | 1 + .../elements/button/components/button.tsx | 31 ++++ .../components/styles/button.module.scss | 27 +++ src/components/elements/button/index.ts | 1 + src/components/elements/progress-bar/index.ts | 1 + .../elements/progress-bar/progress-bar.tsx | 9 + .../styles/progressbar.module.scss | 26 +++ .../elements/tooltip/components/tooltip.tsx | 162 ++++++++++++++++++ src/components/elements/tooltip/index.ts | 1 + .../messages/utils/scroll-into-view.ts | 11 ++ 12 files changed, 314 insertions(+) create mode 100644 src/app/api/messages/chat/route.ts create mode 100644 src/components/elements/anchor/components/anchor.tsx create mode 100644 src/components/elements/anchor/index.ts create mode 100644 src/components/elements/button/components/button.tsx create mode 100644 src/components/elements/button/components/styles/button.module.scss create mode 100644 src/components/elements/button/index.ts create mode 100644 src/components/elements/progress-bar/index.ts create mode 100644 src/components/elements/progress-bar/progress-bar.tsx create mode 100644 src/components/elements/progress-bar/styles/progressbar.module.scss create mode 100644 src/components/elements/tooltip/components/tooltip.tsx create mode 100644 src/components/elements/tooltip/index.ts create mode 100644 src/features/messages/utils/scroll-into-view.ts diff --git a/src/app/api/messages/chat/route.ts b/src/app/api/messages/chat/route.ts new file mode 100644 index 00000000..9a720015 --- /dev/null +++ b/src/app/api/messages/chat/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; + +import { prisma } from "@/lib/prisma"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const conversation_id = searchParams.get("conversation_id") as string; + + const messageSchema = z.string(); + const zod = messageSchema.safeParse(conversation_id); + + if (!zod.success) { + return NextResponse.json({ error: zod.error.formErrors }, { status: 400 }); + } + + try { + const chat = await prisma.message.findMany({ + where: { + conversation_id: conversation_id, + }, + }); + + return NextResponse.json(chat, { status: 200 }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/components/elements/anchor/components/anchor.tsx b/src/components/elements/anchor/components/anchor.tsx new file mode 100644 index 00000000..06703f25 --- /dev/null +++ b/src/components/elements/anchor/components/anchor.tsx @@ -0,0 +1,16 @@ +import Link, { LinkProps } from "next/link"; + +import { cn } from "@/utils/cn"; + +interface ILink extends LinkProps { + children: React.ReactNode; + className?: string; +} + +export const Anchor = ({ children, className, ...props }: ILink) => { + return ( + + {children} + + ); +}; diff --git a/src/components/elements/anchor/index.ts b/src/components/elements/anchor/index.ts new file mode 100644 index 00000000..d44d8f10 --- /dev/null +++ b/src/components/elements/anchor/index.ts @@ -0,0 +1 @@ +export { Anchor } from "./components/anchor"; diff --git a/src/components/elements/button/components/button.tsx b/src/components/elements/button/components/button.tsx new file mode 100644 index 00000000..e8f1922a --- /dev/null +++ b/src/components/elements/button/components/button.tsx @@ -0,0 +1,31 @@ +import React, { forwardRef } from "react"; + +import { cn } from "@/utils/cn"; + +interface IButton extends React.ButtonHTMLAttributes { + children: React.ReactNode; +} + +export const Button = forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + ); + }, +); + +Button.displayName = "Button"; diff --git a/src/components/elements/button/components/styles/button.module.scss b/src/components/elements/button/components/styles/button.module.scss new file mode 100644 index 00000000..4b8b5518 --- /dev/null +++ b/src/components/elements/button/components/styles/button.module.scss @@ -0,0 +1,27 @@ +.container { + cursor: pointer; + display: grid; + place-items: center; + padding: 0.5em; + border-radius: 100vmax; + transition: background-color 0.15s ease-in-out; + + svg { + width: var(--fs-h2); + height: var(--fs-h2); + fill: var(--clr-secondary); + } + + &:hover { + background-color: var(--clr-nav-hover); + } + + &:active { + background-color: var(--clr-nav-active); + } + + &:focus-visible { + outline: 2px solid var(--clr-secondary); + background-color: var(--clr-nav-hover); + } +} diff --git a/src/components/elements/button/index.ts b/src/components/elements/button/index.ts new file mode 100644 index 00000000..72cf883a --- /dev/null +++ b/src/components/elements/button/index.ts @@ -0,0 +1 @@ +export { Button } from "./components/button"; diff --git a/src/components/elements/progress-bar/index.ts b/src/components/elements/progress-bar/index.ts new file mode 100644 index 00000000..5d1a89ff --- /dev/null +++ b/src/components/elements/progress-bar/index.ts @@ -0,0 +1 @@ +export { ProgressBar } from "./progress-bar"; diff --git a/src/components/elements/progress-bar/progress-bar.tsx b/src/components/elements/progress-bar/progress-bar.tsx new file mode 100644 index 00000000..1032d6dc --- /dev/null +++ b/src/components/elements/progress-bar/progress-bar.tsx @@ -0,0 +1,9 @@ +import styles from "./styles/progressbar.module.scss"; + +export const ProgressBar = () => { + return ( +
+ +
+ ); +}; diff --git a/src/components/elements/progress-bar/styles/progressbar.module.scss b/src/components/elements/progress-bar/styles/progressbar.module.scss new file mode 100644 index 00000000..df55a358 --- /dev/null +++ b/src/components/elements/progress-bar/styles/progressbar.module.scss @@ -0,0 +1,26 @@ +.progressbar { + position: relative; + height: 3px; + background-color: var(--clr-background); + overflow: hidden; + + span { + position: absolute; + display: block; + height: 100%; + width: 80px; + border-radius: 100vmax; + background-color: var(--clr-primary); + animation: progress 1s linear infinite; + } +} + +@keyframes progress { + 0% { + left: 0; + } + + 100% { + left: 100%; + } +} diff --git a/src/components/elements/tooltip/components/tooltip.tsx b/src/components/elements/tooltip/components/tooltip.tsx new file mode 100644 index 00000000..b4f99b36 --- /dev/null +++ b/src/components/elements/tooltip/components/tooltip.tsx @@ -0,0 +1,162 @@ +"use client"; +import React, { FC, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +import { cn } from "@/utils/cn"; + +interface ITooltip extends React.HTMLAttributes { + text: string; + delay?: number; + minWidth?: number; + maxWidth?: number; + offset?: number; + children: React.ReactNode; +} + +type TooltipContent = Omit & { + parentRef: React.RefObject; +}; + +export const Tooltip: FC = ({ + children, + className, + delay = 500, + ...props +}) => { + const [displayTooltip, setDisplayTooltip] = useState(false); + const divRef = useRef(null); + const [hovering, setHovering] = useState(false); + + useEffect(() => { + const timeout = setTimeout(() => { + if (hovering) setDisplayTooltip(true); + }, delay); + + return () => { + clearTimeout(timeout); + }; + }, [hovering, delay]); + + const handleMouseEnter = () => { + setHovering(true); + }; + + const handleMouseLeave = () => { + setHovering(false); + setDisplayTooltip(false); + }; + + const handleFocus = () => { + if (!displayTooltip) setDisplayTooltip(true); + }; + + const handleBlur = () => { + setDisplayTooltip(false); + }; + + const element = divRef?.current?.querySelector("button"); + const isDisabled = element?.disabled; + + return ( +
+ {displayTooltip && !isDisabled && ( + + )} + {children} +
+ ); +}; + +const TooltipContent = ({ + text, + minWidth, + maxWidth, + offset = 2, + parentRef, +}: TooltipContent) => { + const tooltipRef = useRef(null); + const [parentBoundaries, setParentBoundaries] = useState( + null, + ); + const [tooltipBoundaries, setTooltipBoundaries] = useState( + null, + ); + + useLayoutEffect(() => { + if (parentRef?.current) { + setParentBoundaries(parentRef?.current.getBoundingClientRect()); + } + + if (tooltipRef.current) { + setTooltipBoundaries(tooltipRef.current.getBoundingClientRect()); + } + }, [parentRef, tooltipRef]); + + const innerWidth = window.innerWidth; + const innerHeight = window.innerHeight; + + if ( + (minWidth && innerWidth < minWidth) || + (maxWidth && innerWidth > maxWidth) + ) { + return null; + } + + const styles: React.CSSProperties = { + position: "fixed", + }; + + if (tooltipBoundaries && parentBoundaries) { + if (parentBoundaries?.bottom + tooltipBoundaries?.height > innerHeight) { + styles.top = parentBoundaries?.top - tooltipBoundaries?.height - offset; + } else { + styles.top = parentBoundaries?.bottom + offset; + } + + if (tooltipBoundaries?.width > innerWidth) { + styles.left = 0; + styles.right = 0; + } else if ( + parentBoundaries?.left + + parentBoundaries?.width / 2 - + tooltipBoundaries?.width / 2 <= + 0 + ) { + styles.left = 0; + } else if ( + parentBoundaries?.left + + parentBoundaries?.width / 2 + + tooltipBoundaries?.width / 2 > + innerWidth - 0 + ) { + styles.right = 0; + } else { + styles.left = + parentBoundaries?.left + + parentBoundaries?.width / 2 - + tooltipBoundaries?.width / 2; + } + } + + return createPortal( +
+ {text} +
, + document.body, + ); +}; diff --git a/src/components/elements/tooltip/index.ts b/src/components/elements/tooltip/index.ts new file mode 100644 index 00000000..ca119a49 --- /dev/null +++ b/src/components/elements/tooltip/index.ts @@ -0,0 +1 @@ +export { Tooltip } from "./components/tooltip"; diff --git a/src/features/messages/utils/scroll-into-view.ts b/src/features/messages/utils/scroll-into-view.ts new file mode 100644 index 00000000..95b00e49 --- /dev/null +++ b/src/features/messages/utils/scroll-into-view.ts @@ -0,0 +1,11 @@ +type ScrollIntoView = { + element: Element | null; + behavior?: "smooth" | "auto" | "instant"; +}; + +export const scrollIntoView = ({ + element, + behavior = "instant", +}: ScrollIntoView) => { + if (element) element.scrollIntoView({ behavior }); +}; From ab15b7356d2e0bc84bfd791275cfa4f628c7f805 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 16 Feb 2024 00:35:03 +0400 Subject: [PATCH 02/25] chore: bump version number to 1.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2998a8f7..2040a19e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chirp", "description": "Chirp is a social media app built with Next.js, Prisma, and Supabase", - "version": "1.9.0", + "version": "1.9.1", "private": true, "scripts": { "dev": "next dev", From 0f041f43a2f6ca0d9d152876c42f8a5e29005cd9 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Fri, 16 Feb 2024 00:51:27 +0400 Subject: [PATCH 03/25] chore: remove screen-reader-only links --- src/app/layout.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 905a76ea..c24084e8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,14 +37,6 @@ export default async function RootLayout({ lang="en" > - - Skip to home timeline - - - - Skip to trending - -
From cdec67e94604fc3187677f43ce3a9de6a6a1869f Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 17 Feb 2024 21:41:26 +0400 Subject: [PATCH 04/25] chore: add docker support --- .dockerignore | 56 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 64 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 15 +++++++++++ 3 files changed, 135 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..61b8ef6b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +### STANDARD GIT IGNORE FILE ### + +# DEPENDENCIES +node_modules/ +/.pnp +.pnp.js +package-lock.json +yarn.lock + +# TESTING +/coverage +*.lcov +.nyc_output + +# BUILD +build/ +public/build/ +dist/ +generated/ + +# ENV FILES +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# LOGS +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# MISC +.idea +.turbo/ +.cache/ +.next/ +.nuxt/ +tmp/ +temp/ +.eslintcache +.docusaurus + +# MAC +._* +.DS_Store +Thumbs.db + +.turbo +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2a872070 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=20.10.0 +FROM node:${NODE_VERSION}-slim as base + +LABEL fly_launch_runtime="Next.js/Prisma" + +# Next.js/Prisma app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + +ARG NEXT_PUBLIC_SUPABASE_URL +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL + +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY + +# Install pnpm +ARG PNPM_VERSION=8.15.1 +RUN npm install -g pnpm@$PNPM_VERSION + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp openssl pkg-config python-is-python3 + +# Install node modules +COPY --link package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod=false + +# Generate Prisma Client +COPY --link prisma . +RUN npx prisma generate + +# Copy application code +COPY --link . . + +# Build application +RUN pnpm run build + +# Remove development dependencies +RUN pnpm prune --prod + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y openssl && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["pnpm", "start"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..3785b8d3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.8" +services: + app: + build: + context: . + args: + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} + image: chirp + ports: + - "8080:8080" + env_file: + - .env + volumes: + - .:/app From 7ad039676f9c2c141abf539cd69f130859176742 Mon Sep 17 00:00:00 2001 From: davitJabushanuri Date: Sat, 17 Feb 2024 21:41:40 +0400 Subject: [PATCH 05/25] chore: remove cypress --- cypress/e2e/1-getting-started/todo.cy.js | 142 -------- cypress/e2e/2-advanced-examples/actions.cy.js | 315 ------------------ .../e2e/2-advanced-examples/aliasing.cy.js | 43 --- .../e2e/2-advanced-examples/assertions.cy.js | 175 ---------- .../e2e/2-advanced-examples/connectors.cy.js | 96 ------ cypress/e2e/2-advanced-examples/cookies.cy.js | 79 ----- .../e2e/2-advanced-examples/cypress_api.cy.js | 213 ------------ cypress/e2e/2-advanced-examples/files.cy.js | 89 ----- .../2-advanced-examples/local_storage.cy.js | 58 ---- .../e2e/2-advanced-examples/location.cy.js | 34 -- cypress/e2e/2-advanced-examples/misc.cy.js | 106 ------ .../e2e/2-advanced-examples/navigation.cy.js | 56 ---- .../network_requests.cy.js | 184 ---------- .../e2e/2-advanced-examples/querying.cy.js | 106 ------ .../spies_stubs_clocks.cy.js | 215 ------------ .../e2e/2-advanced-examples/traversal.cy.js | 116 ------- .../e2e/2-advanced-examples/utilities.cy.js | 109 ------ .../e2e/2-advanced-examples/viewport.cy.js | 59 ---- cypress/e2e/2-advanced-examples/waiting.cy.js | 33 -- cypress/e2e/2-advanced-examples/window.cy.js | 22 -- cypress/fixtures/example.json | 5 - cypress/support/commands.ts | 39 --- cypress/support/component-index.html | 14 - cypress/support/component.ts | 30 -- cypress/support/e2e.ts | 20 -- cypress/tsconfig.json | 26 -- 26 files changed, 2384 deletions(-) delete mode 100644 cypress/e2e/1-getting-started/todo.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/actions.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/aliasing.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/assertions.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/connectors.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/cookies.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/cypress_api.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/files.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/local_storage.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/location.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/misc.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/navigation.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/network_requests.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/querying.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/traversal.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/utilities.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/viewport.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/waiting.cy.js delete mode 100644 cypress/e2e/2-advanced-examples/window.cy.js delete mode 100644 cypress/fixtures/example.json delete mode 100644 cypress/support/commands.ts delete mode 100644 cypress/support/component-index.html delete mode 100644 cypress/support/component.ts delete mode 100644 cypress/support/e2e.ts delete mode 100644 cypress/tsconfig.json diff --git a/cypress/e2e/1-getting-started/todo.cy.js b/cypress/e2e/1-getting-started/todo.cy.js deleted file mode 100644 index 5bb1a687..00000000 --- a/cypress/e2e/1-getting-started/todo.cy.js +++ /dev/null @@ -1,142 +0,0 @@ -/// -// Welcome to Cypress! -// -// This spec file contains a variety of sample tests -// for a todo list app that are designed to demonstrate -// the power of writing tests in Cypress. -// -// To learn more about how Cypress works and -// what makes it such an awesome testing tool, -// please read our getting started guide: -// https://on.cypress.io/introduction-to-cypress - -describe("example to-do app", () => { - beforeEach(() => { - // Cypress starts out with a blank slate for each test - // so we must tell it to visit our website with the `cy.visit()` command. - // Since we want to visit the same URL at the start of all our tests, - // we include it in our beforeEach function so that it runs before each test - cy.visit("https://example.cypress.io/todo"); - }); - - it("displays two todo items by default", () => { - // We use the `cy.get()` command to get all elements that match the selector. - // Then, we use `should` to assert that there are two matched items, - // which are the two default items. - cy.get(".todo-list li").should("have.length", 2); - - // We can go even further and check that the default todos each contain - // the correct text. We use the `first` and `last` functions - // to get just the first and last matched elements individually, - // and then perform an assertion with `should`. - cy.get(".todo-list li").first().should("have.text", "Pay electric bill"); - cy.get(".todo-list li").last().should("have.text", "Walk the dog"); - }); - - it("can add new todo items", () => { - // We'll store our item text in a variable so we can reuse it - const newItem = "Feed the cat"; - - // Let's get the input element and use the `type` command to - // input our new list item. After typing the content of our item, - // we need to type the enter key as well in order to submit the input. - // This input has a data-test attribute so we'll use that to select the - // element in accordance with best practices: - // https://on.cypress.io/selecting-elements - cy.get("[data-test=new-todo]").type(`${newItem}{enter}`); - - // Now that we've typed our new item, let's check that it actually was added to the list. - // Since it's the newest item, it should exist as the last element in the list. - // In addition, with the two default items, we should have a total of 3 elements in the list. - // Since assertions yield the element that was asserted on, - // we can chain both of these assertions together into a single statement. - cy.get(".todo-list li") - .should("have.length", 3) - .last() - .should("have.text", newItem); - }); - - it("can check off an item as completed", () => { - // In addition to using the `get` command to get an element by selector, - // we can also use the `contains` command to get an element by its contents. - // However, this will yield the