From 8d0bcb717161ac12b32c2fbf1b492cdc8de5d311 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Wed, 7 May 2025 21:55:22 +0200 Subject: [PATCH 01/13] Refactor post and comment handling; enhance sharing logic Refactored post and comment functionalities by introducing `MemePost` and optimizing metadata handling. Updated comment scroll behavior and added an advanced sharing flow via Web Share API and Clipboard. Adjusted configurations and documentation for frontend and container compatibility improvements. --- docker-compose.frontend.dev.yml | 116 +++++++++++++ docs/example.env | 2 + docs/example.env.frontend | 21 +++ docs/local-development.md | 25 ++- wsd-frontend/next.config.mjs | 108 ++++++------ .../posts/[hex]/comment/[commentHex]/page.tsx | 18 ++ .../src/app/(app)/posts/[hex]/page.tsx | 134 ++------------- .../wsd/MemeComment/MemeComment.tsx | 73 ++++++++- .../src/components/wsd/MemePost/MemePost.tsx | 155 ++++++++++++++++++ .../src/components/wsd/MemePost/index.ts | 1 + wsd-frontend/src/lib/monkeypatches.ts | 2 +- wsd-frontend/src/lib/utils.ts | 4 +- wsd-server/template.nginx.conf | 2 +- 13 files changed, 473 insertions(+), 188 deletions(-) create mode 100644 docker-compose.frontend.dev.yml create mode 100644 docs/example.env.frontend create mode 100644 wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx create mode 100644 wsd-frontend/src/components/wsd/MemePost/MemePost.tsx create mode 100644 wsd-frontend/src/components/wsd/MemePost/index.ts diff --git a/docker-compose.frontend.dev.yml b/docker-compose.frontend.dev.yml new file mode 100644 index 0000000..45c40a5 --- /dev/null +++ b/docker-compose.frontend.dev.yml @@ -0,0 +1,116 @@ +services: + backend: + image: wsd:latest + build: + context: ./wsd + dockerfile: Dockerfile + env_file: .env + networks: + - internal + - external + volumes: + - user_uploads:/wsd/mediafiles + depends_on: + database: + condition: service_healthy + restart: unless-stopped + + server: + build: + context: ./wsd-server + dockerfile: Dockerfile + env_file: .env + ports: + - "${WSD_SERVER_PORT}:80" + depends_on: + backend: + condition: service_started + networks: + internal: + external: + aliases: + - "${WSD_SERVER_NETWORK_APEX_DOMAIN_ALIAS}" + - "${WSD_SERVER_NETWORK_API_SUBDOMAIN_ALIAS}" + - "${WSD_SERVER_NETWORK_AUTH_SUBDOMAIN_ALIAS}" + - "${WSD_SERVER_NETWORK_ADMIN_SUBDOMAIN_ALIAS}" + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + + database: + build: + context: ./wsd-database + dockerfile: Dockerfile + env_file: .env + environment: + POSTGRES_DB: ${WSD__DB__NAME} + POSTGRES_USER: ${WSD__DB__USER} + POSTGRES_PASSWORD: ${WSD__DB__PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - internal + healthcheck: + test: [ "CMD", "pg_isready", "-U", "${WSD__DB__USER}" ] + interval: 10s + timeout: 10s + retries: 10 + restart: unless-stopped + + rabbitmq: + image: rabbitmq:3 + env_file: .env + networks: + - internal + restart: unless-stopped + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 10s + timeout: 10s + retries: 10 + + celery: + image: wsd:latest + command: celery -A wsd worker --pool=threads --concurrency=10 # threads option is important for tensorflow + env_file: .env + volumes: + - user_uploads:/wsd/mediafiles + depends_on: + database: + condition: service_healthy + rabbitmq: + condition: service_healthy + backend: + condition: service_started + networks: + - internal + - external # Needs to download static files and send errors to sentry + restart: unless-stopped + + celery-beat: + image: wsd:latest + command: celery -A wsd beat + env_file: .env + volumes: + - user_uploads:/wsd/mediafiles + depends_on: + database: + condition: service_healthy + rabbitmq: + condition: service_healthy + backend: + condition: service_started + networks: + - internal + - external # Needs to download static files and send errors to sentry + restart: unless-stopped + +volumes: + postgres_data: + user_uploads: + +networks: + internal: + internal: true + external: + driver: bridge diff --git a/docs/example.env b/docs/example.env index ca37022..88a6d8b 100644 --- a/docs/example.env +++ b/docs/example.env @@ -26,6 +26,8 @@ WSD__HOSTS__API_SUBDOMAIN="api" WSD__HOSTS__ADMIN_SUBDOMAIN="admin" WSD__HOSTS__AUTH_SUBDOMAIN="auth" WSD__HOSTS__MEDIA_SUBDOMAIN="media" +# WSD__HOSTS__FRONTEND_DOMAIN="host.docker.internal" # for local development if you want to run the frontend on your host machine +WSD__HOSTS__FRONTEND_DOMAIN="frontend" # if you want to run it in a container WSD__OAUTH__GOOGLE__CLIENT_ID="" WSD__OAUTH__GOOGLE__CLIENT_SECRET="" diff --git a/docs/example.env.frontend b/docs/example.env.frontend new file mode 100644 index 0000000..edd7c71 --- /dev/null +++ b/docs/example.env.frontend @@ -0,0 +1,21 @@ +# Only things that are needed for the frontend should be in this file. +NEXT_PUBLIC_WSD__DEBUG="false" +NEXT_PUBLIC_WSD__PROTOCOL="http" + +NEXT_PUBLIC_WSD__NAME="WSD" +NEXT_PUBLIC_WSD__MOTTO="Why So Locally Dank?" +NEXT_PUBLIC_WSD__DOMAIN="local-whysodank.com" +NEXT_PUBLIC_WSD__API__BASE_URL="http://api.local-whysodank.com" +NEXT_PUBLIC_WSD__API__AUTH_BASE_URL="http://auth.local-whysodank.com" +NEXT_PUBLIC_WSD__LANGUAGE="en" + +NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__DSN="" +NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__TRACES_SAMPLE_RATE="" +NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__REPLAYS_SESSION_SAMPLE_RATE="" +NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__REPLAYS_ON_ERROR_SAMPLE_RATE="" + +NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__GOOGLE_ANALYTICS__GA_ID="" +NEXT_PUBLIC_WSD__VERIFICATION__MICROSOFT__ASSOCIATED_APPLICATION_ID="" +NEXT_PUBLIC_WSD__GITHUB_LINK="" + +SENTRY_AUTH_TOKEN="" diff --git a/docs/local-development.md b/docs/local-development.md index 56525c3..a69344f 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -11,7 +11,8 @@ The project assumes it is running on a domain instead of localhost, which has some drawbacks. To avoid said drawbacks, we use the hosts file to create our local domain to develop on. -I picked `local-whysodank.com` as the domain but you can pick anything and change the environment variables in .env file. +I picked `local-whysodank.com` as the domain but you can pick anything and change the environment variables in .env +file. - Add the following entries to your hosts file: (for linux this file is commonly located at commonly in `/etc/hosts`) @@ -23,14 +24,28 @@ I picked `local-whysodank.com` as the domain but you can pick anything and chang 127.0.0.1 media.local-whysodank.com ``` -- Copy `example.env` to a new file `.env` in project root and update the domain values if you are not using `local-whysodank.com` +- Copy `example.env` to a new file `.env` in project root and update the domain values if you are not using + `local-whysodank.com` +- Only if you want to run the frontend appart from the docker: Copy `example.env.frontend` to a new file `.env` in + the `wsd-frontend` folder and update the domain values if you are not using `local-whysodank.com` ## Run the project -- `docker compose build` Running `up` instead may give you errors about not being able to pull wsd:latest since it is built locally and reused -- `docker compose up` After building it first, the wsd:latest will be reused successfull and everything should work out of the box +- `docker compose up --build` This will build the images and start the containers (`--build` is optional if you have + already built the images and didn't change anything) +- `docker compose up -d` This will start the containers in detached mode +- `docker compose build frontend/backend/server/database` This will build the images for the frontend, backend, server + and database if you dont want to rebuild everything + +## Run everything except the frontend + +- `docker compose up -f docker-compose.frontend.dev.yml up --build` This will build the images and start the containers +- (in the `wsd-frontend` folder) `npm run dev` This will start the frontend in development mode (this is only needed if + you want to run the frontend + outside of docker) ## Caveats -- Doesn't have volumes mounted for the actual workdir so you have to rebuild the image every time you change something (you can add the volumes manually and do `git update-index --assume-unchanged docker-compose.yml`) +- Doesn't have volumes mounted for the actual workdir so you have to rebuild the image every time you change something ( + you can add the volumes manually and do `git update-index --assume-unchanged docker-compose.yml`) - It probably doesn't work in Apple Silicon and maybe not even on Windows. diff --git a/wsd-frontend/next.config.mjs b/wsd-frontend/next.config.mjs index 7e3428c..b6fa0e1 100644 --- a/wsd-frontend/next.config.mjs +++ b/wsd-frontend/next.config.mjs @@ -1,73 +1,75 @@ -import {withSentryConfig} from '@sentry/nextjs'; -import {createRequire} from "module"; +import { withSentryConfig } from '@sentry/nextjs' -const require = createRequire(import.meta.url); +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) /** @type {import('next').NextConfig} */ const nextConfig = { - experimental: { - // For forbidden interrupts - authInterrupts: true, - optimizePackageImports: ["lodash"] - }, - turbopack: { - rules: { - '*.svg': { - loaders: ['@svgr/webpack'], - as: "*.js" - }, - } + experimental: { + // For forbidden interrupts + authInterrupts: true, + optimizePackageImports: ['lodash'], + }, + allowedDevOrigins: ['localhost:3000', 'local-whysodank.com'], + turbopack: { + rules: { + '*.svg': { + loaders: ['@svgr/webpack'], + as: '*.js', + }, }, - webpack(config) { - config.module.rules.push({ - test: /\.svg$/i, - use: ['@svgr/webpack'], - }); + }, + webpack(config) { + config.module.rules.push({ + test: /\.svg$/i, + use: ['@svgr/webpack'], + }) - return config - } -}; + return config + }, +} const withBundleAnalyzer = require('@next/bundle-analyzer')({ - enabled: process.env.ANALYZE === 'true', + enabled: process.env.ANALYZE === 'true', }) export default withSentryConfig(withBundleAnalyzer(nextConfig), { - // For all available options, see: - // https://github.com/getsentry/sentry-webpack-plugin#options + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options - org: "why-so-dank", - project: "wsd-front-end", + org: 'why-so-dank', + project: 'wsd-front-end', - // Only print logs for uploading source maps in CI - silent: !process.env.CI, + // Only print logs for uploading source maps in CI + silent: !process.env.CI, - // For all available options, see: - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - // Upload a larger set of source maps for prettier stack traces (increases build time) - widenClientFileUpload: true, + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, - // Automatically annotate React components to show their full name in breadcrumbs and session replay - reactComponentAnnotation: { - enabled: true, - }, + // Automatically annotate React components to show their full name in breadcrumbs and session replay + reactComponentAnnotation: { + enabled: true, + }, - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. - // This can increase your server load as well as your hosting bill. - // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- - // side errors will fail. - tunnelRoute: "/monitoring", + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + tunnelRoute: '/monitoring', - // Hides source maps from generated client bundles - hideSourceMaps: true, + // Hides source maps from generated client bundles + hideSourceMaps: true, - // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, - // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) - // See the following for more information: - // https://docs.sentry.io/product/crons/ - // https://vercel.com/docs/cron-jobs - automaticVercelMonitors: true, -}); + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // See the following for more information: + // https://docs.sentry.io/product/crons/ + // https://vercel.com/docs/cron-jobs + automaticVercelMonitors: true, +}) diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx new file mode 100644 index 0000000..8b633a0 --- /dev/null +++ b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx @@ -0,0 +1,18 @@ +import { type Metadata } from 'next' +import { notFound } from 'next/navigation' + +import MemePost, { generatePostAndCommentMetadata } from '@/components/wsd/MemePost/MemePost' + +export async function generateMetadata(props: { + params: Promise<{ hex: string; commentHex: string }> +}): Promise { + return (await generatePostAndCommentMetadata(props)) ?? notFound() +} + +export default async function CommentPostPage(props: { + params: Promise<{ hex: string; commentHex: string }> + searchParams: Promise +}) { + console.log('OMMENT') + return +} diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx index bc0bebd..4de9934 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx @@ -1,127 +1,19 @@ -import type { Metadata } from 'next' -import Link from 'next/link' +import { type Metadata } from 'next' import { notFound } from 'next/navigation' -import _ from 'lodash' +import MemePost, { generatePostAndCommentMetadata } from '@/components/wsd/MemePost/MemePost' -import { Button, buttonVariants } from '@/components/shadcn/button' -import { Overlay, OverlayClose, OverlayContent, OverlayTitle, OverlayTrigger } from '@/components/shadcn/overlay' -import { Separator } from '@/components/shadcn/separator' -import AuthenticatedOnlyActionButton from '@/components/wsd/AuthenticatedOnlyActionButton' -import Meme from '@/components/wsd/Meme' -import MemeComment from '@/components/wsd/MemeComment' -import NewComment from '@/components/wsd/NewComment' - -import { APIQuery, APIType, includesType } from '@/api' -import config from '@/config' -import { getWSDMetadata } from '@/lib/metadata' -import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' -import { getKeys } from '@/lib/typeHelpers' -import { InvalidHEXError, cn, hexToUUIDv4, suppress } from '@/lib/utils' - -export async function generateMetadata(props: { params: Promise<{ hex: string }> }): Promise { - const params = await props.params - const wsd = sUseWSDAPI() - const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) - - if (!_.isUndefined(postId)) { - const { data: post } = await wsd.post(postId, { include: 'tags' }) - if (!_.isUndefined(post)) { - return await getWSDMetadata({ - title: post.title, - description: post.title, - image: post.is_nsfw ? config.nsfw_image : post.image, - }) - } - } - return notFound() +export async function generateMetadata(props: { params: Promise<{ hex: string }> }): Promise { + return (await generatePostAndCommentMetadata(props)) ?? notFound() } -export default async function PostPage(props: { - params: Promise<{ hex: string }> - searchParams: Promise> -}) { - const searchParams = await props.searchParams - const params = await props.params - const wsd = sUseWSDAPI() - const isAuthenticated = await wsd.isAuthenticated() - const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) - - const orderingLabels = { - created_at: 'Oldest', - '-created_at': 'Newest', - '-positive_vote_count': 'Most Liked', - '-negative_vote_count': 'Most Disliked', - } - - function newOrderingHREF(ordering: APIQuery<'/v0/post-comments/'>['ordering']) { - return { pathname: `/posts/${params.hex}`, query: { ...searchParams, ordering } } - } - - const currentOrdering = _.get(orderingLabels, searchParams.ordering || 'created_at', orderingLabels.created_at) - - if (!_.isUndefined(postId)) { - const { data: post } = await wsd.post(postId, { include: 'tags,user' }) - if (!_.isUndefined(post)) { - const { data: comments } = await wsd.postComments({ - post: post.id, - include: 'user', - ordering: searchParams?.ordering || 'positive_vote_count', - }) - const post_ = includesType(includesType(post as APIType<'Post'>, 'user', 'User'), 'tags', 'PostTag', true) - return ( -
-
- - - {wsd.hasResults(comments) && ( - - - - - - Ordering -
- {getKeys(orderingLabels).map((key) => ( - - - {orderingLabels[key]} - - - ))} -
-
-
- )} - {!isAuthenticated && ( -
- - Be the first one to comment! - -
- )} - {isAuthenticated && wsd.hasNoResult(comments) && ( -
- Be the first one to comment! -
- )} - {isAuthenticated && } -
- {wsd.hasResults(comments) && - comments.results.map((comment) => ( - - ))} -
-
-
- ) - } - } - return notFound() +export default async function PostPage(props: { params: Promise<{ hex: string }>; searchParams: Promise }) { + const postHex = (await props.params).hex + const params = new Promise<{ + hex: string + commentHex?: string + }>((resolve) => { + resolve({ hex: postHex, commentHex: undefined }) + }) + return } diff --git a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx index d92fef7..7c93296 100644 --- a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx +++ b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx @@ -1,8 +1,9 @@ 'use client' import Link from 'next/link' +import { useRouter } from 'next/navigation' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import * as Icons from 'lucide-react' @@ -12,15 +13,44 @@ import { WSDEditorRenderer } from '@/components/wsd/WSDEditor/Editor' import type { APIType, Includes } from '@/api' import { useWSDAPI } from '@/lib/serverHooks' -import { cn, shortFormattedDateTime } from '@/lib/utils' +import { InvalidHEXError, cn, shortFormattedDateTime, suppress, uuidV4toHEX } from '@/lib/utils' import { formatDistanceToNow } from 'date-fns' import { toast } from 'sonner' -export function MemeComment({ comment }: { comment: Includes, 'user', APIType<'User'>> }) { +export function MemeComment({ + comment, + targeted = false, +}: { + comment: Includes, 'user', APIType<'User'>> + targeted?: boolean +}) { + const router = useRouter() + + const commentRef = useRef(null) const wsd = useWSDAPI() const [feedback, setFeedback] = useState | null>(comment.vote) const [voteCount, setVoteCount] = useState((comment.positive_vote_count || 0) - (comment.negative_vote_count || 0)) + const postId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.post)) + const commentId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.id)) + console.log(postId) + console.log(commentId) + + useEffect(() => { + if (targeted && commentRef.current) { + // Wait for the page to load completely + const scrollToComment = () => { + commentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + + // Use requestIdleCallback for better performance, falling back to setTimeout + if (typeof window.requestIdleCallback !== 'undefined') { + window.requestIdleCallback(scrollToComment) + } else { + setTimeout(scrollToComment, 100) + } + } + }, [targeted]) function handleVote(vote: APIType<'VoteEnum'>) { return async () => { @@ -66,9 +96,34 @@ export function MemeComment({ comment }: { comment: Includes - +
+
@@ -114,6 +169,14 @@ export function MemeComment({ comment }: { comment: Includes +
+ + + Ordering +
+ {getKeys(orderingLabels).map((key) => ( + + + {orderingLabels[key]} + + + ))} +
+
+ + )} + {!isAuthenticated && ( +
+ + Be the first one to comment! + +
+ )} + {isAuthenticated && wsd.hasNoResult(comments) && ( +
+ Be the first one to comment! +
+ )} + {isAuthenticated && } +
+ {wsd.hasResults(comments) && + comments.results.map((comment) => ( + + ))} +
+
+ + ) + } + } + return notFound() +} diff --git a/wsd-frontend/src/components/wsd/MemePost/index.ts b/wsd-frontend/src/components/wsd/MemePost/index.ts new file mode 100644 index 0000000..17a621b --- /dev/null +++ b/wsd-frontend/src/components/wsd/MemePost/index.ts @@ -0,0 +1 @@ +export { default as MemePage, generatePostAndCommentMetadata } from './MemePost' diff --git a/wsd-frontend/src/lib/monkeypatches.ts b/wsd-frontend/src/lib/monkeypatches.ts index dcae5bd..0bd821d 100644 --- a/wsd-frontend/src/lib/monkeypatches.ts +++ b/wsd-frontend/src/lib/monkeypatches.ts @@ -32,7 +32,7 @@ export const noDirectConsoleLog = withAttributes Date: Thu, 8 May 2025 16:16:39 +0200 Subject: [PATCH 02/13] Reverted Local Frontend --- docker-compose.frontend.dev.yml | 116 -------------------------------- docker-compose.yml | 4 +- docs/example.env.frontend | 21 ------ docs/local-development.md | 27 ++------ wsd-server/template.nginx.conf | 2 +- 5 files changed, 11 insertions(+), 159 deletions(-) delete mode 100644 docker-compose.frontend.dev.yml delete mode 100644 docs/example.env.frontend diff --git a/docker-compose.frontend.dev.yml b/docker-compose.frontend.dev.yml deleted file mode 100644 index 45c40a5..0000000 --- a/docker-compose.frontend.dev.yml +++ /dev/null @@ -1,116 +0,0 @@ -services: - backend: - image: wsd:latest - build: - context: ./wsd - dockerfile: Dockerfile - env_file: .env - networks: - - internal - - external - volumes: - - user_uploads:/wsd/mediafiles - depends_on: - database: - condition: service_healthy - restart: unless-stopped - - server: - build: - context: ./wsd-server - dockerfile: Dockerfile - env_file: .env - ports: - - "${WSD_SERVER_PORT}:80" - depends_on: - backend: - condition: service_started - networks: - internal: - external: - aliases: - - "${WSD_SERVER_NETWORK_APEX_DOMAIN_ALIAS}" - - "${WSD_SERVER_NETWORK_API_SUBDOMAIN_ALIAS}" - - "${WSD_SERVER_NETWORK_AUTH_SUBDOMAIN_ALIAS}" - - "${WSD_SERVER_NETWORK_ADMIN_SUBDOMAIN_ALIAS}" - restart: unless-stopped - extra_hosts: - - "host.docker.internal:host-gateway" - - database: - build: - context: ./wsd-database - dockerfile: Dockerfile - env_file: .env - environment: - POSTGRES_DB: ${WSD__DB__NAME} - POSTGRES_USER: ${WSD__DB__USER} - POSTGRES_PASSWORD: ${WSD__DB__PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - internal - healthcheck: - test: [ "CMD", "pg_isready", "-U", "${WSD__DB__USER}" ] - interval: 10s - timeout: 10s - retries: 10 - restart: unless-stopped - - rabbitmq: - image: rabbitmq:3 - env_file: .env - networks: - - internal - restart: unless-stopped - healthcheck: - test: [ "CMD", "rabbitmqctl", "status" ] - interval: 10s - timeout: 10s - retries: 10 - - celery: - image: wsd:latest - command: celery -A wsd worker --pool=threads --concurrency=10 # threads option is important for tensorflow - env_file: .env - volumes: - - user_uploads:/wsd/mediafiles - depends_on: - database: - condition: service_healthy - rabbitmq: - condition: service_healthy - backend: - condition: service_started - networks: - - internal - - external # Needs to download static files and send errors to sentry - restart: unless-stopped - - celery-beat: - image: wsd:latest - command: celery -A wsd beat - env_file: .env - volumes: - - user_uploads:/wsd/mediafiles - depends_on: - database: - condition: service_healthy - rabbitmq: - condition: service_healthy - backend: - condition: service_started - networks: - - internal - - external # Needs to download static files and send errors to sentry - restart: unless-stopped - -volumes: - postgres_data: - user_uploads: - -networks: - internal: - internal: true - external: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 235ee0e..84af269 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,7 +81,7 @@ services: networks: - internal healthcheck: - test: ["CMD", "pg_isready", "-U", "${WSD__DB__USER}"] + test: [ "CMD", "pg_isready", "-U", "${WSD__DB__USER}" ] interval: 10s timeout: 10s retries: 10 @@ -90,6 +90,8 @@ services: rabbitmq: image: rabbitmq:3 env_file: .env + ports: + - "5672:5672" networks: - internal restart: unless-stopped diff --git a/docs/example.env.frontend b/docs/example.env.frontend deleted file mode 100644 index edd7c71..0000000 --- a/docs/example.env.frontend +++ /dev/null @@ -1,21 +0,0 @@ -# Only things that are needed for the frontend should be in this file. -NEXT_PUBLIC_WSD__DEBUG="false" -NEXT_PUBLIC_WSD__PROTOCOL="http" - -NEXT_PUBLIC_WSD__NAME="WSD" -NEXT_PUBLIC_WSD__MOTTO="Why So Locally Dank?" -NEXT_PUBLIC_WSD__DOMAIN="local-whysodank.com" -NEXT_PUBLIC_WSD__API__BASE_URL="http://api.local-whysodank.com" -NEXT_PUBLIC_WSD__API__AUTH_BASE_URL="http://auth.local-whysodank.com" -NEXT_PUBLIC_WSD__LANGUAGE="en" - -NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__DSN="" -NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__TRACES_SAMPLE_RATE="" -NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__REPLAYS_SESSION_SAMPLE_RATE="" -NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__SENTRY__REPLAYS_ON_ERROR_SAMPLE_RATE="" - -NEXT_PUBLIC_WSD__FRONT_END__DEVTOOLS__GOOGLE_ANALYTICS__GA_ID="" -NEXT_PUBLIC_WSD__VERIFICATION__MICROSOFT__ASSOCIATED_APPLICATION_ID="" -NEXT_PUBLIC_WSD__GITHUB_LINK="" - -SENTRY_AUTH_TOKEN="" diff --git a/docs/local-development.md b/docs/local-development.md index a69344f..d157dad 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -11,8 +11,7 @@ The project assumes it is running on a domain instead of localhost, which has some drawbacks. To avoid said drawbacks, we use the hosts file to create our local domain to develop on. -I picked `local-whysodank.com` as the domain but you can pick anything and change the environment variables in .env -file. +I picked `local-whysodank.com` as the domain but you can pick anything and change the environment variables in .env file. - Add the following entries to your hosts file: (for linux this file is commonly located at commonly in `/etc/hosts`) @@ -24,28 +23,16 @@ file. 127.0.0.1 media.local-whysodank.com ``` -- Copy `example.env` to a new file `.env` in project root and update the domain values if you are not using - `local-whysodank.com` -- Only if you want to run the frontend appart from the docker: Copy `example.env.frontend` to a new file `.env` in - the `wsd-frontend` folder and update the domain values if you are not using `local-whysodank.com` +- Copy `example.env` to a new file `.env` in project root and update the domain values if you are not using `local-whysodank.com` ## Run the project -- `docker compose up --build` This will build the images and start the containers (`--build` is optional if you have - already built the images and didn't change anything) -- `docker compose up -d` This will start the containers in detached mode -- `docker compose build frontend/backend/server/database` This will build the images for the frontend, backend, server - and database if you dont want to rebuild everything - -## Run everything except the frontend - -- `docker compose up -f docker-compose.frontend.dev.yml up --build` This will build the images and start the containers -- (in the `wsd-frontend` folder) `npm run dev` This will start the frontend in development mode (this is only needed if - you want to run the frontend - outside of docker) +`docker compose up` should build the images and start the containers. +- `docker compose build` Running `up` instead may give you errors about not being able to pull wsd:latest since it is built locally and reused +- `docker compose up` After building it first, the wsd:latest will be reused successfull and everything should work out of the box ## Caveats -- Doesn't have volumes mounted for the actual workdir so you have to rebuild the image every time you change something ( - you can add the volumes manually and do `git update-index --assume-unchanged docker-compose.yml`) +It probably doesn't work in Apple Silicon and maybe not even on Windows. +- Doesn't have volumes mounted for the actual workdir so you have to rebuild the image every time you change something (you can add the volumes manually and do `git update-index --assume-unchanged docker-compose.yml`) - It probably doesn't work in Apple Silicon and maybe not even on Windows. diff --git a/wsd-server/template.nginx.conf b/wsd-server/template.nginx.conf index 5ac8e45..589f978 100644 --- a/wsd-server/template.nginx.conf +++ b/wsd-server/template.nginx.conf @@ -17,7 +17,7 @@ http { server_name ${WSD__HOSTS__DOMAIN}; location / { - proxy_pass http://${WSD__HOSTS__FRONTEND_DOMAIN}:3000; + proxy_pass http://frontend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; From e0c28d584df024868fd2597ea5ddc98b60bf745b Mon Sep 17 00:00:00 2001 From: Pdzly Date: Thu, 8 May 2025 17:08:12 +0200 Subject: [PATCH 03/13] Remove console.log statements from MemeComment component Removed unnecessary console.log statements used for debugging postId and commentId. This cleanup improves code readability and avoids cluttering the console during runtime. --- wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx index 7c93296..4713dcd 100644 --- a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx +++ b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx @@ -33,8 +33,6 @@ export function MemeComment({ const [voteCount, setVoteCount] = useState((comment.positive_vote_count || 0) - (comment.negative_vote_count || 0)) const postId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.post)) const commentId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.id)) - console.log(postId) - console.log(commentId) useEffect(() => { if (targeted && commentRef.current) { From 409f858e9e5c6a0ee3847352951e08bd07662c8f Mon Sep 17 00:00:00 2001 From: Rooki <34943569+Pdzly@users.noreply.github.com> Date: Thu, 8 May 2025 20:10:53 +0200 Subject: [PATCH 04/13] Update local-development.md --- docs/local-development.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/local-development.md b/docs/local-development.md index d157dad..56525c3 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -27,12 +27,10 @@ I picked `local-whysodank.com` as the domain but you can pick anything and chang ## Run the project -`docker compose up` should build the images and start the containers. - `docker compose build` Running `up` instead may give you errors about not being able to pull wsd:latest since it is built locally and reused - `docker compose up` After building it first, the wsd:latest will be reused successfull and everything should work out of the box ## Caveats -It probably doesn't work in Apple Silicon and maybe not even on Windows. - Doesn't have volumes mounted for the actual workdir so you have to rebuild the image every time you change something (you can add the volumes manually and do `git update-index --assume-unchanged docker-compose.yml`) - It probably doesn't work in Apple Silicon and maybe not even on Windows. From 1a538e13a1e225643497728a6d3e29b024241c60 Mon Sep 17 00:00:00 2001 From: Rooki <34943569+Pdzly@users.noreply.github.com> Date: Thu, 8 May 2025 20:11:26 +0200 Subject: [PATCH 05/13] Update example.env --- docs/example.env | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/example.env b/docs/example.env index 88a6d8b..ca37022 100644 --- a/docs/example.env +++ b/docs/example.env @@ -26,8 +26,6 @@ WSD__HOSTS__API_SUBDOMAIN="api" WSD__HOSTS__ADMIN_SUBDOMAIN="admin" WSD__HOSTS__AUTH_SUBDOMAIN="auth" WSD__HOSTS__MEDIA_SUBDOMAIN="media" -# WSD__HOSTS__FRONTEND_DOMAIN="host.docker.internal" # for local development if you want to run the frontend on your host machine -WSD__HOSTS__FRONTEND_DOMAIN="frontend" # if you want to run it in a container WSD__OAUTH__GOOGLE__CLIENT_ID="" WSD__OAUTH__GOOGLE__CLIENT_SECRET="" From c376a9cfcc12ab5cec7063814ef014d20e63c22d Mon Sep 17 00:00:00 2001 From: Rooki <34943569+Pdzly@users.noreply.github.com> Date: Thu, 8 May 2025 20:12:06 +0200 Subject: [PATCH 06/13] Update docker-compose.yml --- docker-compose.yml | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 84af269..dee4744 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,6 @@ services: database: condition: service_healthy restart: unless-stopped - frontend: build: context: ./wsd-frontend @@ -44,7 +43,6 @@ services: backend: condition: service_started restart: unless-stopped - server: build: context: ./wsd-server @@ -66,7 +64,6 @@ services: - "${WSD_SERVER_NETWORK_AUTH_SUBDOMAIN_ALIAS}" - "${WSD_SERVER_NETWORK_ADMIN_SUBDOMAIN_ALIAS}" restart: unless-stopped - database: build: context: ./wsd-database @@ -81,26 +78,44 @@ services: networks: - internal healthcheck: - test: [ "CMD", "pg_isready", "-U", "${WSD__DB__USER}" ] + test: ["CMD", "pg_isready", "-U", "${WSD__DB__USER}"] interval: 10s timeout: 10s retries: 10 - restart: unless-stopped + + + + + + + Expand All + + @@ -90,6 +90,8 @@ services: + + restart: unless-stopped rabbitmq: image: rabbitmq:3 env_file: .env - ports: - - "5672:5672" networks: - internal restart: unless-stopped + + + + + + + + Expand Down + + + healthcheck: test: [ "CMD", "rabbitmqctl", "status" ] interval: 10s timeout: 10s retries: 10 - celery: image: wsd:latest command: celery -A wsd worker --pool=threads --concurrency=10 # threads option is important for tensorflow @@ -118,7 +133,6 @@ services: - internal - external # Needs to download static files and send errors to sentry restart: unless-stopped - celery-beat: image: wsd:latest command: celery -A wsd beat @@ -136,13 +150,11 @@ services: - internal - external # Needs to download static files and send errors to sentry restart: unless-stopped - volumes: postgres_data: user_uploads: - networks: internal: internal: true external: - driver: bridge +driver: bridge From c6dcabd3dea9f359c899eaec2d4334405126d2ea Mon Sep 17 00:00:00 2001 From: Rooki <34943569+Pdzly@users.noreply.github.com> Date: Thu, 8 May 2025 20:12:43 +0200 Subject: [PATCH 07/13] Update docker-compose.yml --- docker-compose.yml | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index dee4744..235ee0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: database: condition: service_healthy restart: unless-stopped + frontend: build: context: ./wsd-frontend @@ -43,6 +44,7 @@ services: backend: condition: service_started restart: unless-stopped + server: build: context: ./wsd-server @@ -64,6 +66,7 @@ services: - "${WSD_SERVER_NETWORK_AUTH_SUBDOMAIN_ALIAS}" - "${WSD_SERVER_NETWORK_ADMIN_SUBDOMAIN_ALIAS}" restart: unless-stopped + database: build: context: ./wsd-database @@ -82,40 +85,20 @@ services: interval: 10s timeout: 10s retries: 10 - - - - - - - - Expand All - - @@ -90,6 +90,8 @@ services: - restart: unless-stopped + rabbitmq: image: rabbitmq:3 env_file: .env networks: - internal restart: unless-stopped - - - - - - - - Expand Down - - - healthcheck: test: [ "CMD", "rabbitmqctl", "status" ] interval: 10s timeout: 10s retries: 10 + celery: image: wsd:latest command: celery -A wsd worker --pool=threads --concurrency=10 # threads option is important for tensorflow @@ -133,6 +116,7 @@ services: - internal - external # Needs to download static files and send errors to sentry restart: unless-stopped + celery-beat: image: wsd:latest command: celery -A wsd beat @@ -150,11 +134,13 @@ services: - internal - external # Needs to download static files and send errors to sentry restart: unless-stopped + volumes: postgres_data: user_uploads: + networks: internal: internal: true external: -driver: bridge + driver: bridge From ab793976fc736073a0d8caec1e0bb1c0b20674d5 Mon Sep 17 00:00:00 2001 From: Rooki <34943569+Pdzly@users.noreply.github.com> Date: Thu, 8 May 2025 20:13:08 +0200 Subject: [PATCH 08/13] Update monkeypatches.ts --- wsd-frontend/src/lib/monkeypatches.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsd-frontend/src/lib/monkeypatches.ts b/wsd-frontend/src/lib/monkeypatches.ts index 0bd821d..dcae5bd 100644 --- a/wsd-frontend/src/lib/monkeypatches.ts +++ b/wsd-frontend/src/lib/monkeypatches.ts @@ -32,7 +32,7 @@ export const noDirectConsoleLog = withAttributes Date: Fri, 16 May 2025 19:58:49 +0200 Subject: [PATCH 09/13] Reverted next.config.mjs --- wsd-frontend/next.config.mjs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/wsd-frontend/next.config.mjs b/wsd-frontend/next.config.mjs index b6fa0e1..5e76ec8 100644 --- a/wsd-frontend/next.config.mjs +++ b/wsd-frontend/next.config.mjs @@ -1,34 +1,32 @@ -import { withSentryConfig } from '@sentry/nextjs' +import {withSentryConfig} from '@sentry/nextjs'; +import {createRequire} from "module"; -import { createRequire } from 'module' - -const require = createRequire(import.meta.url) +const require = createRequire(import.meta.url); /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { // For forbidden interrupts authInterrupts: true, - optimizePackageImports: ['lodash'], + optimizePackageImports: ["lodash"] }, - allowedDevOrigins: ['localhost:3000', 'local-whysodank.com'], turbopack: { rules: { '*.svg': { loaders: ['@svgr/webpack'], - as: '*.js', + as: "*.js" }, - }, + } }, webpack(config) { config.module.rules.push({ test: /\.svg$/i, use: ['@svgr/webpack'], - }) + }); return config - }, -} + } +}; const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', @@ -38,8 +36,8 @@ export default withSentryConfig(withBundleAnalyzer(nextConfig), { // For all available options, see: // https://github.com/getsentry/sentry-webpack-plugin#options - org: 'why-so-dank', - project: 'wsd-front-end', + org: "why-so-dank", + project: "wsd-front-end", // Only print logs for uploading source maps in CI silent: !process.env.CI, @@ -59,7 +57,7 @@ export default withSentryConfig(withBundleAnalyzer(nextConfig), { // This can increase your server load as well as your hosting bill. // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- // side errors will fail. - tunnelRoute: '/monitoring', + tunnelRoute: "/monitoring", // Hides source maps from generated client bundles hideSourceMaps: true, @@ -72,4 +70,4 @@ export default withSentryConfig(withBundleAnalyzer(nextConfig), { // https://docs.sentry.io/product/crons/ // https://vercel.com/docs/cron-jobs automaticVercelMonitors: true, -}) +}); From 2702e6320edb0d7078a5a7016d7befaa5f673bb5 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Fri, 16 May 2025 20:00:18 +0200 Subject: [PATCH 10/13] Remove leftover debug log from comment page component Removed an unnecessary console.log statement that appeared to be a debugging artifact. This change cleans up the code and improves maintainability by removing unused code. --- .../src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx index 8b633a0..56e58a2 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx @@ -13,6 +13,5 @@ export default async function CommentPostPage(props: { params: Promise<{ hex: string; commentHex: string }> searchParams: Promise }) { - console.log('OMMENT') return } From b270b1509feea34978bab174dacb85f6932df6d7 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Fri, 16 May 2025 20:04:29 +0200 Subject: [PATCH 11/13] Moved Metadata logik back to the page.tsx --- .../posts/[hex]/comment/[commentHex]/page.tsx | 49 +++++++++++++++++-- .../src/app/(app)/posts/[hex]/page.tsx | 27 ++++++++-- .../src/components/wsd/MemePost/MemePost.tsx | 42 ---------------- .../src/components/wsd/MemePost/index.ts | 2 +- 4 files changed, 70 insertions(+), 50 deletions(-) diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx index 56e58a2..a9a96af 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx @@ -1,12 +1,53 @@ import { type Metadata } from 'next' import { notFound } from 'next/navigation' -import MemePost, { generatePostAndCommentMetadata } from '@/components/wsd/MemePost/MemePost' +import _ from 'lodash' + +import MemePost from '@/components/wsd/MemePost/MemePost' + +import { includesType } from '@/api' +import config from '@/config' +import { getWSDMetadata } from '@/lib/metadata' +import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' +import { InvalidHEXError, hexToUUIDv4, suppress } from '@/lib/utils' export async function generateMetadata(props: { - params: Promise<{ hex: string; commentHex: string }> -}): Promise { - return (await generatePostAndCommentMetadata(props)) ?? notFound() + params: Promise<{ hex: string; commentHex?: string }> +}): Promise { + const params = await props.params + const wsd = sUseWSDAPI() + const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) + const commentParamId = params.commentHex + + if (!_.isUndefined(postId)) { + const { data: post } = await wsd.post(postId, { include: 'tags' }) + if (!_.isUndefined(post)) { + if (!_.isUndefined(commentParamId)) { + const commentId = suppress([InvalidHEXError], () => hexToUUIDv4(commentParamId)) + if (!_.isUndefined(commentId)) { + const { data: comment } = await wsd.postComment(commentId, { include: 'user' }) + if (!_.isUndefined(comment)) { + const commentData = includesType(comment, 'user', 'User') + + // @TODO: Handle comment body + return await getWSDMetadata({ + title: `${commentData.user.username} - commented on ${post.title}`, + image: post.is_nsfw ? config.nsfw_image : post.image, + }) + } else { + return notFound() + } + } + } + + return await getWSDMetadata({ + title: post.title, + description: post.title, + image: post.is_nsfw ? config.nsfw_image : post.image, + }) + } + } + return notFound() } export default async function CommentPostPage(props: { diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx index 4de9934..a818efb 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx @@ -1,10 +1,31 @@ import { type Metadata } from 'next' import { notFound } from 'next/navigation' -import MemePost, { generatePostAndCommentMetadata } from '@/components/wsd/MemePost/MemePost' +import _ from 'lodash' -export async function generateMetadata(props: { params: Promise<{ hex: string }> }): Promise { - return (await generatePostAndCommentMetadata(props)) ?? notFound() +import MemePost from '@/components/wsd/MemePost/MemePost' + +import config from '@/config' +import { getWSDMetadata } from '@/lib/metadata' +import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' +import { InvalidHEXError, hexToUUIDv4, suppress } from '@/lib/utils' + +export async function generateMetadata(props: { params: Promise<{ hex: string }> }): Promise { + const params = await props.params + const wsd = sUseWSDAPI() + const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) + + if (!_.isUndefined(postId)) { + const { data: post } = await wsd.post(postId, { include: 'tags' }) + if (!_.isUndefined(post)) { + return await getWSDMetadata({ + title: post.title, + description: post.title, + image: post.is_nsfw ? config.nsfw_image : post.image, + }) + } + } + return notFound() } export default async function PostPage(props: { params: Promise<{ hex: string }>; searchParams: Promise }) { diff --git a/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx index a0bd8e9..54c52bf 100644 --- a/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx +++ b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx @@ -1,4 +1,3 @@ -import type { Metadata } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' @@ -13,51 +12,10 @@ import MemeComment from '@/components/wsd/MemeComment' import NewComment from '@/components/wsd/NewComment' import { APIQuery, APIType, includesType } from '@/api' -import config from '@/config' -import { getWSDMetadata } from '@/lib/metadata' import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' import { getKeys } from '@/lib/typeHelpers' import { InvalidHEXError, cn, hexToUUIDv4, suppress } from '@/lib/utils' -export async function generatePostAndCommentMetadata(props: { - params: Promise<{ hex: string; commentHex?: string }> -}): Promise { - const params = await props.params - const wsd = sUseWSDAPI() - const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) - const commentParamId = params.commentHex - - if (!_.isUndefined(postId)) { - const { data: post } = await wsd.post(postId, { include: 'tags' }) - if (!_.isUndefined(post)) { - if (!_.isUndefined(commentParamId)) { - const commentId = suppress([InvalidHEXError], () => hexToUUIDv4(commentParamId)) - if (!_.isUndefined(commentId)) { - const { data: comment } = await wsd.postComment(commentId, { include: 'user' }) - if (!_.isUndefined(comment)) { - const commentData = includesType(comment, 'user', 'User') - - // @TODO: Handle comment body - return await getWSDMetadata({ - title: `${commentData.user.username} - commented on ${post.title}`, - image: post.is_nsfw ? config.nsfw_image : post.image, - }) - } else { - return notFound() - } - } - } - - return await getWSDMetadata({ - title: post.title, - description: post.title, - image: post.is_nsfw ? config.nsfw_image : post.image, - }) - } - } - return notFound() -} - export default async function MemePost(props: { params: Promise<{ hex: string; commentHex?: string }> searchParams: Promise> diff --git a/wsd-frontend/src/components/wsd/MemePost/index.ts b/wsd-frontend/src/components/wsd/MemePost/index.ts index 17a621b..6145fc9 100644 --- a/wsd-frontend/src/components/wsd/MemePost/index.ts +++ b/wsd-frontend/src/components/wsd/MemePost/index.ts @@ -1 +1 @@ -export { default as MemePage, generatePostAndCommentMetadata } from './MemePost' +export { default as MemePage } from './MemePost' From 328f628dd7e73f1c9be67eb8c0f3f6dd2fb795e7 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Fri, 16 May 2025 20:57:50 +0200 Subject: [PATCH 12/13] Refactor scroll logic with ScrollToHashContainer in comments Replaced custom scroll logic in `MemeComment` with the reusable `ScrollToHashContainer` component. Improved code structure by simplifying properties while preserving existing functionality for smooth scrolling and highlighting targeted comments. --- .../shadcn/scroll-to-hash-container.tsx | 24 ++- .../wsd/MemeComment/MemeComment.tsx | 153 ++++++++---------- 2 files changed, 88 insertions(+), 89 deletions(-) diff --git a/wsd-frontend/src/components/shadcn/scroll-to-hash-container.tsx b/wsd-frontend/src/components/shadcn/scroll-to-hash-container.tsx index 1408f20..b0528a4 100644 --- a/wsd-frontend/src/components/shadcn/scroll-to-hash-container.tsx +++ b/wsd-frontend/src/components/shadcn/scroll-to-hash-container.tsx @@ -1,28 +1,38 @@ 'use client' -import { useEffect, useRef, PropsWithChildren } from 'react' +import { PropsWithChildren, useEffect, useRef } from 'react' type ScrollToHashContainerProps = PropsWithChildren<{ - hash: string + hash?: string + shouldScroll?: boolean offset?: number + className?: string }> -function ScrollToHashContainer({ hash, offset = 0, children }: ScrollToHashContainerProps) { +function ScrollToHashContainer({ hash, shouldScroll, className, offset = 0, children }: ScrollToHashContainerProps) { const ref = useRef(null) useEffect(() => { - const shouldScroll = window.location.hash === `#${hash}` - if (shouldScroll && ref.current) { + if (hash === undefined && shouldScroll === undefined) return + let shouldScrollPage = false + if (hash && window.location.hash === `#${hash}`) { + shouldScrollPage = true + } + if (shouldScroll !== undefined && shouldScroll !== shouldScrollPage) { + shouldScrollPage = shouldScroll + } + if (shouldScrollPage && ref.current) { requestAnimationFrame(() => { if (!ref.current) return const top = ref.current.offsetTop - offset window.scrollTo({ top, behavior: 'smooth' }) }) } - }, [hash]) + }, [hash, shouldScroll, offset]) return ( -
{/* Adding id here causes the browser's auto scroll behavior to kick in */} +
{/* Adding id here causes the browser's auto scroll behavior to kick in */} {children}
) diff --git a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx index 4713dcd..469fefd 100644 --- a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx +++ b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx @@ -3,11 +3,12 @@ import Link from 'next/link' import { useRouter } from 'next/navigation' -import { useEffect, useRef, useState } from 'react' +import { useState } from 'react' import * as Icons from 'lucide-react' import { Button } from '@/components/shadcn/button' +import { ScrollToHashContainer } from '@/components/shadcn/scroll-to-hash-container' import UserAvatar from '@/components/wsd/UserAvatar' import { WSDEditorRenderer } from '@/components/wsd/WSDEditor/Editor' @@ -27,29 +28,12 @@ export function MemeComment({ }) { const router = useRouter() - const commentRef = useRef(null) const wsd = useWSDAPI() const [feedback, setFeedback] = useState | null>(comment.vote) const [voteCount, setVoteCount] = useState((comment.positive_vote_count || 0) - (comment.negative_vote_count || 0)) const postId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.post)) const commentId = suppress([InvalidHEXError], () => uuidV4toHEX(comment.id)) - useEffect(() => { - if (targeted && commentRef.current) { - // Wait for the page to load completely - const scrollToComment = () => { - commentRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - } - - // Use requestIdleCallback for better performance, falling back to setTimeout - if (typeof window.requestIdleCallback !== 'undefined') { - window.requestIdleCallback(scrollToComment) - } else { - setTimeout(scrollToComment, 100) - } - } - }, [targeted]) - function handleVote(vote: APIType<'VoteEnum'>) { return async () => { let newVoteCount = voteCount @@ -113,76 +97,81 @@ export function MemeComment({ console.log(e) toast("Couldn't share the comment") } - router.replace(`/posts/${postId}/comment/${commentId}`, { scroll: false }) + router.push(`/posts/${postId}/comment/${commentId}`, { scroll: false }) } return ( -
- - - -
-
- - {comment.user.username} - - - {formatDistanceToNow(new Date(comment.created_at), { addSuffix: true })} - -
-
- -
-
-
- - - {voteCount} - - + {comment.user.username} + + + {formatDistanceToNow(new Date(comment.created_at), { addSuffix: true })} + +
+
+
-
- +
+
+ + + {voteCount} + + +
+
+ +
-
-
+
+ ) } From bd7f6d574ccdefc7c17d2835e5af9e33fc50d089 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Tue, 3 Jun 2025 17:48:05 +0200 Subject: [PATCH 13/13] Refactor WSD API hook usage and enhance `includesType` call in MemePost component --- .../(app)/posts/[hex]/comment/[commentHex]/page.tsx | 4 ++-- wsd-frontend/src/components/wsd/MemePost/MemePost.tsx | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx index a9a96af..00cc22f 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx @@ -8,14 +8,14 @@ import MemePost from '@/components/wsd/MemePost/MemePost' import { includesType } from '@/api' import config from '@/config' import { getWSDMetadata } from '@/lib/metadata' -import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' +import { getWSDAPI } from '@/lib/serverHooks' import { InvalidHEXError, hexToUUIDv4, suppress } from '@/lib/utils' export async function generateMetadata(props: { params: Promise<{ hex: string; commentHex?: string }> }): Promise { const params = await props.params - const wsd = sUseWSDAPI() + const wsd = getWSDAPI() const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) const commentParamId = params.commentHex diff --git a/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx index 54c52bf..8797121 100644 --- a/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx +++ b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx @@ -12,7 +12,7 @@ import MemeComment from '@/components/wsd/MemeComment' import NewComment from '@/components/wsd/NewComment' import { APIQuery, APIType, includesType } from '@/api' -import { useWSDAPI as sUseWSDAPI } from '@/lib/serverHooks' +import { getWSDAPI } from '@/lib/serverHooks' import { getKeys } from '@/lib/typeHelpers' import { InvalidHEXError, cn, hexToUUIDv4, suppress } from '@/lib/utils' @@ -22,7 +22,7 @@ export default async function MemePost(props: { }) { const searchParams = await props.searchParams const params = await props.params - const wsd = sUseWSDAPI() + const wsd = getWSDAPI() const isAuthenticated = await wsd.isAuthenticated() const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) const targetCommentId = !_.isUndefined(params.commentHex) @@ -50,7 +50,11 @@ export default async function MemePost(props: { include: 'user', ordering: searchParams?.ordering || 'positive_vote_count', }) - const post_ = includesType(includesType(post as APIType<'Post'>, 'user', 'User'), 'tags', 'PostTag', true) + const post_ = includesType( + includesType(includesType(post as APIType<'Post'>, 'user', 'User'), 'tags', 'PostTag', true), + 'category', + 'PostCategory' + ) return (