Repository: rubyr/markdump
Pull request #32: [WIP] Add adult flagging to posts
Author: login: Sciman101
Created: 2025-04-14T04:07:47Z | Merged: 2025-04-24T06:29:11Z

- Added 'adult content' toggle to the post editor
- Shows whether posts contain adult content within the post list
- Shows whether posts contain adult content within the post header
- Added 'modal' component
 - When a user clicks on an adult post, the modal will pop up and say they need to be 18+ to proceed. They're given the option to either proceed or go back.

Closes #21 

Linked issue #21: adult/18+ content flag
posts should be able to be tagged as adult, which would show a warning next to them in post lists and require verification before viewing. a simple "must be 18+" page would be fine here. this data is already in the backend, we just need to add a checkbox in the post editor and the aforementioned elements
Linked issue comments: user: login: Sciman101
created_at: 2025-04-03T04:49:01Z
body: Should the 'latest posts' list on the homepage hide 18+ by default?
author_association: COLLABORATOR

Diff:
diff --git a/src/components/adult-badge.astro b/src/components/adult-badge.astro
new file mode 100644
index 0000000..42bf1d3
--- /dev/null
+++ b/src/components/adult-badge.astro
@@ -0,0 +1,12 @@
+<div class="adult-badge">18+</div>
+
+<style>
+    .adult-badge {
+        background-color: var(--danger);
+        color: var(--heading-color);
+        padding: calc(var(--spacing-base) / 5) calc(var(--spacing-base) / 3);
+        border-radius: var(--border-radius);
+        width: fit-content;
+        align-self: flex-end;
+    }
+</style>
\ No newline at end of file
diff --git a/src/components/modal/modal.module.css b/src/components/modal/modal.module.css
new file mode 100644
index 0000000..5f32cc3
--- /dev/null
+++ b/src/components/modal/modal.module.css
@@ -0,0 +1,40 @@
+dialog:modal {
+    position: absolute;
+    left: 50%;
+    top: 40%;
+    transform: translate(-50%, -40%);
+    border: none;
+    padding: var(--spacing-base);
+    border-radius: var(--border-radius-md);
+
+    color: var(--body-color);
+    background: var(--gradient-secondary);
+
+    .modalChildren {
+        display: flex;
+        flex-direction: column;
+    }
+
+    h1 {
+        margin-top: 0;
+    }
+
+    .modalCloseButton {
+        position: absolute;
+        right: var(--spacing-base);
+        top: var(--spacing-base);
+        margin: 0;
+        padding: 0.2rem;
+        height: 2em;
+        width: 2em;
+        min-width: 0;
+    }
+}
+
+dialog::backdrop {
+    background: rgba(0,0,0,0.5);
+}
+
+.hiddenBackground::backdrop {
+    backdrop-filter: blur(12px);
+}
\ No newline at end of file
diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx
new file mode 100644
index 0000000..c96d827
--- /dev/null
+++ b/src/components/modal/modal.tsx
@@ -0,0 +1,53 @@
+import type React from "preact/compat";
+import { useEffect, useRef } from "preact/hooks";
+import styles from './modal.module.css';
+
+type ModalProps = {
+    isOpen: boolean;
+    onClose?: () => void;
+    hasCloseButton?: boolean;
+    children: React.ReactNode;
+    hidePage?: boolean;
+}
+
+const setDisableScrolling = (disabled:boolean) => {
+    document.body.style.overflow = disabled ? 'hidden' : 'auto';
+}
+
+const Modal = ({children, isOpen, hasCloseButton = true, hidePage = false,onClose}:ModalProps) => {
+
+    const modalRef = useRef<HTMLDialogElement>(null);
+
+    useEffect(() => {
+        const modalElement = modalRef.current;
+        if (!modalElement) return;
+
+        if (isOpen) {
+            modalElement.showModal();
+            setDisableScrolling(true);
+        }else{
+            modalElement.close();
+            setDisableScrolling(false);
+        }
+    },[isOpen]);
+
+    const handleCloseModal = () => {
+        if (onClose) {
+            onClose();
+        }
+        setDisableScrolling(false);
+        modalRef.current?.close();
+    }
+
+    return (
+        <dialog ref={modalRef} className={hidePage ? styles.hiddenBackground : undefined}>
+            {hasCloseButton && <button class={styles.modalCloseButton} onClick={handleCloseModal} aria-label="Close">X</button>}
+            <div class={styles.modalChildren}>
+                {children}
+            </div>
+        </dialog>
+    );
+
+};
+
+export default Modal;
\ No newline at end of file
diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx
index cf97c44..c3081b0 100644
--- a/src/components/post-form/post-form.tsx
+++ b/src/components/post-form/post-form.tsx
@@ -56,6 +56,14 @@ const PostForm = ({ onSubmit, errors, data, children }: PostFormProps) => {
                     />{' '}
                     Unlisted (exclude this from search results)
                 </label>
+                <label>
+                    <input
+                        type="checkbox"
+                        name="adult_content"
+                        checked={data?.adult_content ?? false}
+                    />{' '}
+                    This post contains adult content (18+)
+                </label>
                 {children}
                 <button class="success">Submit</button>
             </section>
diff --git a/src/components/post-list.astro b/src/components/post-list.astro
index f354fd3..88f7424 100644
--- a/src/components/post-list.astro
+++ b/src/components/post-list.astro
@@ -1,6 +1,7 @@
 ---
+import AdultBadge from "./adult-badge.astro";
 import TagList from "./tag-list.astro";
-import Timestamp from "./timestamp";
+import Timestamp from "./timestamp/timestamp";
 
 const { posts } = Astro.props;
 ---
@@ -8,12 +9,17 @@ const { posts } = Astro.props;
 {
     posts?.length ? (
         <ul class="postList">
-            {posts.map((post: { id: any; title: any; created_at: string; tags: string[]; }) => (
+            {posts.map((post: { id: any; title: any; created_at: string; tags: string[]; adult_content: boolean }) => (
                 <li class="postListing">
-                    <span class="title"><a href={`/posts/${post.id}`}>{post.title || 'Untitled'}</a></span>
-                    <div>by Anonymous</div>
-                    <div class="timestamp"><Timestamp client:only="preact" time={post.created_at} /></div>
-                    <TagList tags={post.tags} />
+                    <div class="left">
+                        <span class="title"><a href={`/posts/${post.id}`}>{post.title || 'Untitled'}</a></span>
+                        <div>by Anonymous</div>
+                        <TagList tags={post.tags} />
+                    </div>
+                    <div class="right">
+                        <Timestamp client:only="preact" time={post.created_at} />
+                        {post.adult_content && <AdultBadge/>}
+                    </div>
                 </li>
             ))}
         </ul>
@@ -35,7 +41,8 @@ const { posts } = Astro.props;
     li.postListing {
         text-decoration: none;
         display: flex;
-        flex-direction: column;
+        flex-direction: row;
+        justify-content: space-between;
 
         padding: var(--spacing-base);
 
@@ -52,10 +59,12 @@ const { posts } = Astro.props;
                 width: auto;
             }
         }
-    }
-
-    .timestamp {
-        color: var(--secondary);
-        font-size: calc(var(--font-size-base) * 0.8);
+        .left, .right {
+            display: flex;
+            flex-direction: column;
+        }
+        .right {
+            text-align: right;
+        }
     }
 </style>
diff --git a/src/components/sort-by/show-adult.tsx b/src/components/sort-by/show-adult.tsx
new file mode 100644
index 0000000..fd3fad3
--- /dev/null
+++ b/src/components/sort-by/show-adult.tsx
@@ -0,0 +1,55 @@
+import { useEffect, useState } from 'preact/hooks';
+import styles from './sort-by.module.css';
+import type { ChangeEvent } from 'preact/compat';
+import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
+import type { GenericSchema } from '@supabase/supabase-js/dist/module/lib/types';
+
+/**
+ * Calls the appropriate sort method on the Supabase query, based on the sort
+ * provided.
+ * @param query The query to sort. Call this *after* any filters, and *before*
+ * any modifiers.
+ * @param showAdultPosts Whether or not to include adult posts
+ * @returns 
+ */
+export const filterAdultPostQuery = <
+    S extends GenericSchema,
+    Rw extends Record<string, unknown>,
+    Rs
+>(
+    query: PostgrestFilterBuilder<S, Rw, Rs>,
+    showAdultPosts: boolean
+) => {
+    if (showAdultPosts) {
+        return query;
+    }else{
+        return query.eq('adult_content', false);
+    }
+};
+
+const ShowAdultToggle = ({}) => {
+    const [showAdultPosts, setShowAdultPosts] = useState(false);
+        useEffect(() => {
+            setShowAdultPosts(
+                new URLSearchParams(window.location.search).get('adult') === 'true'
+            );
+        }, []);
+        const selectShowAdult = (e: ChangeEvent) => {
+            const searchParams = new URLSearchParams(window.location.search);
+            if ((e.target as HTMLInputElement).checked) {
+                searchParams.set('adult', 'true');
+            }else{
+                searchParams.delete('adult');
+            }
+            window.location.search = searchParams.toString();
+        };
+
+    return (
+        <div class={styles.sortContainer}>
+            <input type="checkbox" name="show-adult" checked={showAdultPosts} onChange={selectShowAdult}/>            
+            <label for="show-adult">Show Adult Posts</label>
+        </div>
+    );
+};
+
+export default ShowAdultToggle;
diff --git a/src/components/timestamp/timestamp.module.css b/src/components/timestamp/timestamp.module.css
new file mode 100644
index 0000000..ddaac9e
--- /dev/null
+++ b/src/components/timestamp/timestamp.module.css
@@ -0,0 +1,4 @@
+.timestamp {
+    color: var(--secondary);
+    font-size: calc(var(--font-size-base) * 0.8);
+}
\ No newline at end of file
diff --git a/src/components/timestamp.tsx b/src/components/timestamp/timestamp.tsx
similarity index 71%
rename from src/components/timestamp.tsx
rename to src/components/timestamp/timestamp.tsx
index 1750021..bbb2fbc 100644
--- a/src/components/timestamp.tsx
+++ b/src/components/timestamp/timestamp.tsx
@@ -1,4 +1,5 @@
 import { useMemo } from "preact/hooks";
+import styles from './timestamp.module.css';
 
 interface TimestampProps {
     time: string,
@@ -8,7 +9,7 @@ const Timestamp = ({ time }: TimestampProps) => {
     const formattedTime = useMemo(() => new Date(time).toLocaleString(), [time]);
 
     return (
-        <time>{formattedTime}</time>
+        <time className={styles.timestamp}>{formattedTime}</time>
     );
 };
 
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 1c416f7..3332c8c 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -18,6 +18,7 @@ const { data: latest, error } = await supabase()
     .from('posts')
     .select('id, title, created_at, tags')
     .eq('unlisted', false)
+    .eq('adult_content', false) // Do not show adult posts on main page
     .order('created_at', { ascending: false })
     .limit(10);
 ---
diff --git a/src/pages/posts/[postid].astro b/src/pages/posts/[postid].astro
index b8c343e..d25e54b 100644
--- a/src/pages/posts/[postid].astro
+++ b/src/pages/posts/[postid].astro
@@ -3,11 +3,12 @@ import BaseLayout from '../../layouts/base-layout.astro';
 import { marked } from 'marked';
 import sanitizeHtml from 'sanitize-html';
 import { supabase } from '../../util/supabase.astro';
-import Timestamp from '../../components/timestamp';
+import Timestamp from '../../components/timestamp/timestamp';
 import TagList from '../../components/tag-list.astro';
 import Seo from '../../components/seo.astro';
 import CopyToClipboard from '../../components/copy-to-clipboard';
-import err from '../../util/http-error';
+import Modal from '../../components/modal/modal';
+import AdultBadge from '../../components/adult-badge.astro';
 
 const { postid } = Astro.params;
 
@@ -41,10 +42,22 @@ const html = sanitizeHtml(await marked.parse(content.body), {
 const showCopyLinkWarning = Astro.url.searchParams.get('created') === content.edit_key;
 const url = Astro.url.href.split('?')[0];
 const editURL = url + `/edit?key=${content.edit_key}`;
----
 
+const canViewAdultContent = Astro.url.searchParams.get('adult');
+const showAdultWarning = !canViewAdultContent && content.adult_content;
+---
 <BaseLayout title={`${content.title || 'Untitled'}`}>
     <Seo title={content.title} description={trimmedText} />
+
+    <Modal client:only="preact" isOpen={showAdultWarning} hidePage={true} hasCloseButton={false}>
+        <h1>Heads up</h1>
+        <p>The following post contains adult content. By proceeding, you agree that you are at least 18 years old.</p>
+        <div class="modalButtons">
+            <form method="dialog"><button>Ok</button></form>
+            <button onclick="history.back()">Take me back</button>
+        </div>
+    </Modal>
+
     {
         showCopyLinkWarning && (
             <section class="attention">
@@ -95,6 +108,7 @@ const editURL = url + `/edit?key=${content.edit_key}`;
                     </small>
                 )
             }
+            {content.adult_content && <AdultBadge/>}
             <span><a href={`/posts/${postid}/raw`} download={`${content.title}.md`}>Download</a></span>
         </div>
     </section>
@@ -114,6 +128,10 @@ const editURL = url + `/edit?key=${content.edit_key}`;
             margin-bottom: 0;
         }
 
+        .right {
+            text-align: right;
+        }
+
         .title {
             font-size: 1.5rem;
             font-weight: 700;
@@ -132,6 +150,12 @@ const editURL = url + `/edit?key=${content.edit_key}`;
         }
     }
 
+    .modalButtons {
+        display: flex;
+        flex-direction: row;
+        gap: var(--spacing-base);
+    }
+
     .post-body {
         word-break: break-word;
     }
@@ -146,6 +170,10 @@ const editURL = url + `/edit?key=${content.edit_key}`;
 
         background: var(--gradient-secondary);
     }
+
+    .adult-badge {
+        background-color: rgb(233, 70, 70);
+    }
 </style>
 
 <style is:global>
diff --git a/src/pages/search.astro b/src/pages/search.astro
index c67b870..4ade11d 100644
--- a/src/pages/search.astro
+++ b/src/pages/search.astro
@@ -5,6 +5,7 @@ import Pagination from '../components/pagination.astro';
 import PostList from '../components/post-list.astro';
 import Seo from '../components/seo.astro';
 import SortBy, { sortPostQuery, type SortType } from '../components/sort-by/sort-by';
+import ShowAdultToggle, { filterAdultPostQuery } from '../components/sort-by/show-adult';
 
 const query = Astro.url.searchParams.get('q') ?? '';
 
@@ -12,14 +13,16 @@ const pageStr = Astro.url.searchParams.get('p') ?? '1';
 const pageIndex = (parseInt(pageStr) || 1) - 1;
 
 const sortBy = Astro.url.searchParams.get('sort') ?? 'newest';
+const showAdultPosts = Astro.url.searchParams.get('adult') === 'true';
 
 const RESULTS_PER_PAGE = 20;
 
 let dbQuery = supabase()
     .from('posts')
-    .select('title, created_at, tags, id')
+    .select('title, created_at, tags, id, adult_content')
     .eq('unlisted', false)
     .ilike('title', `%${query}%`) // TODO: this is far from a robust search system
+filterAdultPostQuery(dbQuery, showAdultPosts);
 sortPostQuery(dbQuery, sortBy as SortType);
 const { data: posts, error: postsError } = await dbQuery.range(
         pageIndex * RESULTS_PER_PAGE,
@@ -40,6 +43,7 @@ const totalPages = Math.ceil((total ?? 1) / RESULTS_PER_PAGE);
     {query.length > 0 && <h1>Search results for "{query}"</h1>}
     {query.length == 0 && <h1>All posts</h1>}
     <SortBy client:only="preact"/>
+    <ShowAdultToggle client:only="preact"/>
     {(posts?.length ?? 0) > 4 && <Pagination page={pageIndex} totalPages={totalPages} />}
     <PostList posts={posts} />
     <Pagination page={pageIndex} totalPages={totalPages}/>
diff --git a/src/pages/tagged/[tag].astro b/src/pages/tagged/[tag].astro
index da56ca7..ac71559 100644
--- a/src/pages/tagged/[tag].astro
+++ b/src/pages/tagged/[tag].astro
@@ -6,10 +6,12 @@ import PostList from '../../components/post-list.astro';
 import Seo from '../../components/seo.astro';
 import err from '../../util/http-error';
 import SortBy, { sortPostQuery, type SortType } from '../../components/sort-by/sort-by';
+import ShowAdultToggle, { filterAdultPostQuery } from '../../components/sort-by/show-adult';
 
 const { tag } = Astro.params;
 const pageStr = Astro.url.searchParams.get('p') ?? '1';
 const sortBy = Astro.url.searchParams.get('sort') ?? 'newest';
+const showAdultPosts = Astro.url.searchParams.get('adult') === 'true';
 
 if (!tag) {
     return err(404);
@@ -21,9 +23,10 @@ const RESULTS_PER_PAGE = 20;
 
 let dbQuery = supabase()
     .from('posts')
-    .select('title, created_at, tags, id')
+    .select('title, created_at, tags, id, adult_content')
     .eq('unlisted', false)
     .contains('tags', [tag]);
+filterAdultPostQuery(dbQuery, showAdultPosts);
 sortPostQuery(dbQuery, sortBy as SortType);
 const { data: posts, error: postsError } = await dbQuery.range(
         pageIndex * RESULTS_PER_PAGE,
@@ -43,6 +46,7 @@ const totalPages = Math.ceil((total ?? 1) / RESULTS_PER_PAGE);
 <BaseLayout title={`tagged "${tag}"`}>
     <h1>tagged "{tag}"</h1>
     <SortBy client:only="preact"/>
+    <ShowAdultToggle client:only="preact"/>
     {(posts?.length ?? 0) > 4 && <Pagination page={pageIndex} totalPages={totalPages} />}
     <PostList posts={posts} />
     <Pagination page={pageIndex} totalPages={totalPages} />
diff --git a/src/util/parse-post-form-data.ts b/src/util/parse-post-form-data.ts
index 66410be..476a8ef 100644
--- a/src/util/parse-post-form-data.ts
+++ b/src/util/parse-post-form-data.ts
@@ -5,6 +5,7 @@ export interface PostData {
     tags: string[],
     body: string,
     unlisted: boolean,
+    adult_content: boolean,
 }
 
 const parsePostFormData = (data: FormData): PostData => {
@@ -24,11 +25,13 @@ const parsePostFormData = (data: FormData): PostData => {
         allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
     });
     const unlisted = data.get('unlisted') === 'on'; // why.
+    const adult_content = data.get('adult_content') === 'on';
 
     return {
         title,
         tags,
         body,
+        adult_content,
         unlisted
     };
 };


Reviews:
user: login: rubyr
body: quick note on the modal implementation; currently, when a modal is open, the user can still see the page contents beneath, as well as scroll down. we might want to blur the background when a modal is present, and we definitely want to disable scrolling (adding `overflow: hidden` to the body in the modal's open method should work?)
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-04-21T21:29:29Z
user: login: Sciman101
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T21:30:49Z
user: login: rubyr
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-24T06:28:59Z

Review comments:
diff_hunk: @@ -1,19 +1,24 @@
 ---
 import TagList from "./tag-list.astro";
-import Timestamp from "./timestamp";
+import Timestamp from "./timestamp/timestamp";
 
 const { posts } = Astro.props;
 ---
 
 {
     posts?.length ? (
         <ul class="postList">
-            {posts.map((post: { id: any; title: any; created_at: string; tags: string[]; }) => (
+            {posts.map((post: { id: any; title: any; created_at: string; tags: string[]; adult_content: boolean }) => (
                 <li class="postListing">
-                    <span class="title"><a href={`/posts/${post.id}`}>{post.title || 'Untitled'}</a></span>
-                    <div>by Anonymous</div>
-                    <div class="timestamp"><Timestamp client:only="preact" time={post.created_at} /></div>
-                    <TagList tags={post.tags} />
+                    <div class="left">
+                        <span class="title"><a href={`/posts/${post.id}`}>{post.title || 'Untitled'}</a></span>
+                        <div>by Anonymous</div>
+                        <TagList tags={post.tags} />
+                    </div>
+                    <div class="right">
+                        <Timestamp client:only="preact" time={post.created_at} />
+                        {post.adult_content && <div>This post is flagged as 18+</div>}
path: src/components/post-list.astro
user: login: rubyr
body: we could possibly come up with a bit of a snazzier method of displaying this, maybe a little red "18+" box or something
created_at: 2025-04-21T21:21:38Z
author_association: OWNER
diff_hunk: @@ -41,10 +42,22 @@ const html = sanitizeHtml(await marked.parse(content.body), {
 const showCopyLinkWarning = Astro.url.searchParams.get('created') === content.edit_key;
 const url = Astro.url.href.split('?')[0];
 const editURL = url + `/edit?key=${content.edit_key}`;
----
 
+const canViewAdultContent = Astro.url.searchParams.get('adult');
+const showAdultWarning = !canViewAdultContent && content.adult_content;
+---
 <BaseLayout title={`${content.title || 'Untitled'}`}>
     <Seo title={content.title} description={trimmedText} />
+
+    <Modal client:only="preact" isOpen={true} hidePage={true}>
path: src/pages/posts/[postid].astro
user: login: rubyr
body: currently the modal shows for every post, need to check for showAdultWarning
created_at: 2025-04-21T21:22:32Z
author_association: OWNER
diff_hunk: @@ -41,10 +42,22 @@ const html = sanitizeHtml(await marked.parse(content.body), {
 const showCopyLinkWarning = Astro.url.searchParams.get('created') === content.edit_key;
 const url = Astro.url.href.split('?')[0];
 const editURL = url + `/edit?key=${content.edit_key}`;
----
 
+const canViewAdultContent = Astro.url.searchParams.get('adult');
+const showAdultWarning = !canViewAdultContent && content.adult_content;
+---
 <BaseLayout title={`${content.title || 'Untitled'}`}>
     <Seo title={content.title} description={trimmedText} />
+
+    <Modal client:only="preact" isOpen={true} hidePage={true}>
path: src/pages/posts/[postid].astro
user: login: rubyr
body: it's a little unfortunate to have to load preact for every modal, but i'm not sure what a good way around it would be. maybe we can render the modal on the server side, and then only load preact for the buttons? we should discuss
created_at: 2025-04-21T21:23:53Z
author_association: OWNER
diff_hunk: @@ -41,10 +42,22 @@ const html = sanitizeHtml(await marked.parse(content.body), {
 const showCopyLinkWarning = Astro.url.searchParams.get('created') === content.edit_key;
 const url = Astro.url.href.split('?')[0];
 const editURL = url + `/edit?key=${content.edit_key}`;
----
 
+const canViewAdultContent = Astro.url.searchParams.get('adult');
+const showAdultWarning = !canViewAdultContent && content.adult_content;
+---
 <BaseLayout title={`${content.title || 'Untitled'}`}>
     <Seo title={content.title} description={trimmedText} />
+
+    <Modal client:only="preact" isOpen={true} hidePage={true}>
+        <h1>Heads up</h1>
+        <p>The following post contains adult content. By proceeding, you agree that you are at least 18 years old.</p>
+        <div class="modalButtons">
+            <form method="dialog"><button>Ok</button></form>
path: src/pages/posts/[postid].astro
user: login: rubyr
body: it's a little strange to have an "ok" button and an "x" button on this dialog, maybe remove the close button?
created_at: 2025-04-21T21:26:22Z
author_association: OWNER
diff_hunk: @@ -41,10 +42,22 @@ const html = sanitizeHtml(await marked.parse(content.body), {
 const showCopyLinkWarning = Astro.url.searchParams.get('created') === content.edit_key;
 const url = Astro.url.href.split('?')[0];
 const editURL = url + `/edit?key=${content.edit_key}`;
----
 
+const canViewAdultContent = Astro.url.searchParams.get('adult');
+const showAdultWarning = !canViewAdultContent && content.adult_content;
+---
 <BaseLayout title={`${content.title || 'Untitled'}`}>
     <Seo title={content.title} description={trimmedText} />
+
+    <Modal client:only="preact" isOpen={true} hidePage={true}>
path: src/pages/posts/[postid].astro
user: login: Sciman101
body: Whoops
created_at: 2025-04-21T21:30:48Z
author_association: COLLABORATOR

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-14T04:09:40Z
body: ## Deploying markdump with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>c1a104c</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://e8946371.markdump.pages.dev'>https://e8946371.markdump.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://adult-flag.markdump.pages.dev'>https://adult-flag.markdump.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/7677ae96babd4182bf82186310aeaf54/pages/view/markdump/e8946371-d538-4676-86bd-121240f81383)

author_association: NONE
user: login: Sciman101
created_at: 2025-04-24T03:09:43Z
body: ![image](https://github.com/user-attachments/assets/77ca8e46-dbd1-49bf-8a10-e46c14cb15bb)
![image](https://github.com/user-attachments/assets/b0e42108-822f-4b42-a470-19fb1db3a472)
![image](https://github.com/user-attachments/assets/6d1da9e4-77fa-45ea-9f7a-e8017a4350a7)

Tweaked the UI as suggested, made the modal only appear on adult posts.
author_association: COLLABORATOR