diff --git a/wsd-frontend/next.config.mjs b/wsd-frontend/next.config.mjs index a3d9d7d..06d6e35 100644 --- a/wsd-frontend/next.config.mjs +++ b/wsd-frontend/next.config.mjs @@ -42,42 +42,42 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ export default withPWA( 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..00cc22f --- /dev/null +++ b/wsd-frontend/src/app/(app)/posts/[hex]/comment/[commentHex]/page.tsx @@ -0,0 +1,58 @@ +import { type Metadata } from 'next' +import { notFound } from 'next/navigation' + +import _ from 'lodash' + +import MemePost from '@/components/wsd/MemePost/MemePost' + +import { includesType } from '@/api' +import config from '@/config' +import { getWSDMetadata } from '@/lib/metadata' +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 = getWSDAPI() + 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: { + params: Promise<{ hex: string; commentHex: string }> + searchParams: Promise +}) { + return +} diff --git a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx index f03801b..698c8aa 100644 --- a/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx +++ b/wsd-frontend/src/app/(app)/posts/[hex]/page.tsx @@ -1,24 +1,14 @@ -import type { Metadata } from 'next' -import Link from 'next/link' +import { type Metadata } from 'next' import { notFound } from 'next/navigation' import _ from 'lodash' -import { Button, buttonVariants } from '@/components/shadcn/button' -import { Overlay, OverlayClose, OverlayContent, OverlayTitle, OverlayTrigger } from '@/components/shadcn/overlay' -import { ScrollToHashContainer } from '@/components/shadcn/scroll-to-hash-container' -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 MemePost from '@/components/wsd/MemePost/MemePost' -import { APIQuery, APIType, includesType } from '@/api' import config from '@/config' import { getWSDMetadata } from '@/lib/metadata' import { getWSDAPI } from '@/lib/serverHooks' -import { getKeys } from '@/lib/typeHelpers' -import { InvalidHEXError, cn, hexToUUIDv4, suppress } from '@/lib/utils' +import { InvalidHEXError, hexToUUIDv4, suppress } from '@/lib/utils' export async function generateMetadata(props: { params: Promise<{ hex: string }> }): Promise { const params = await props.params @@ -38,6 +28,17 @@ export async function generateMetadata(props: { params: Promise<{ hex: string }> 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 +} +/* export default async function PostPage(props: { params: Promise<{ hex: string }> searchParams: Promise> @@ -132,3 +133,5 @@ export default async function PostPage(props: { } return notFound() } + + */ 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 17fc66a..f692716 100644 --- a/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx +++ b/wsd-frontend/src/components/wsd/MemeComment/MemeComment.tsx @@ -1,26 +1,38 @@ 'use client' import Link from 'next/link' +import { useRouter } from 'next/navigation' 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' import type { APIType, Includes } from '@/api' import { getWSDAPI } 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 wsd = getWSDAPI() 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)) function handleVote(vote: APIType<'VoteEnum'>) { return async () => { @@ -66,62 +78,100 @@ export function MemeComment({ comment }: { comment: Includes - - - -
-
- - {comment.user.username} - - - {formatDistanceToNow(new Date(comment.created_at), { addSuffix: true })} - -
-
- -
-
-
- - - {voteCount} - + {comment.user.username} + + + {formatDistanceToNow(new Date(comment.created_at), { addSuffix: true })} + +
+
+
-
- +
+
+ + + {voteCount} + + +
+
+ +
-
- + + ) } diff --git a/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx new file mode 100644 index 0000000..8797121 --- /dev/null +++ b/wsd-frontend/src/components/wsd/MemePost/MemePost.tsx @@ -0,0 +1,117 @@ +import Link from 'next/link' +import { notFound } from 'next/navigation' + +import _ from 'lodash' + +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 { getWSDAPI } from '@/lib/serverHooks' +import { getKeys } from '@/lib/typeHelpers' +import { InvalidHEXError, cn, hexToUUIDv4, suppress } from '@/lib/utils' + +export default async function MemePost(props: { + params: Promise<{ hex: string; commentHex?: string }> + searchParams: Promise> +}) { + const searchParams = await props.searchParams + const params = await props.params + const wsd = getWSDAPI() + const isAuthenticated = await wsd.isAuthenticated() + const postId = suppress([InvalidHEXError], () => hexToUUIDv4(params.hex)) + const targetCommentId = !_.isUndefined(params.commentHex) + ? suppress([InvalidHEXError], () => hexToUUIDv4(params.commentHex)) + : undefined + + 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(includesType(post as APIType<'Post'>, 'user', 'User'), 'tags', 'PostTag', true), + 'category', + 'PostCategory' + ) + 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() +} 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..6145fc9 --- /dev/null +++ b/wsd-frontend/src/components/wsd/MemePost/index.ts @@ -0,0 +1 @@ +export { default as MemePage } from './MemePost' diff --git a/wsd-frontend/src/lib/utils.ts b/wsd-frontend/src/lib/utils.ts index 6b5c152..6311e0c 100644 --- a/wsd-frontend/src/lib/utils.ts +++ b/wsd-frontend/src/lib/utils.ts @@ -212,8 +212,8 @@ export class InvalidHEXError extends Error { } } -export function hexToUUIDv4(hex: string): string { - if (!/^[0-9a-fA-F]{32}$/.test(hex)) { +export function hexToUUIDv4(hex: string | undefined): string { + if (hex === undefined || !/^[0-9a-fA-F]{32}$/.test(hex)) { throw new InvalidHEXError('Invalid hex string. Must be 32 hexadecimal characters.') } return stringify(Buffer.from(hex, 'hex')) diff --git a/wsd-server/template.nginx.conf b/wsd-server/template.nginx.conf index 97a275f..589f978 100644 --- a/wsd-server/template.nginx.conf +++ b/wsd-server/template.nginx.conf @@ -107,7 +107,7 @@ http { proxy_set_header Referer $http_referer; proxy_cache_bypass $http_upgrade; - add_header Cache-Control "public, max-age=${WSD__STORAGE__S3__PROXY_CACHE_LENGTH}, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable"; } } }