From f5997d120856ccc579e8370c16fee82ec9faa5ce Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:48:34 -0500 Subject: [PATCH 01/12] feat: add the ability to create and edit piefed polls --- src/features/create-post.tsx | 164 +++++++++++++++++++++++++- src/lib/api/adapters/api-blueprint.ts | 15 +++ src/lib/api/adapters/piefed.ts | 32 ++++- src/lib/api/adapters/support.ts | 4 + src/stores/create-post.ts | 41 ++++++- 5 files changed, 249 insertions(+), 7 deletions(-) diff --git a/src/features/create-post.tsx b/src/features/create-post.tsx index 5dfe9cb9..1192e293 100644 --- a/src/features/create-post.tsx +++ b/src/features/create-post.tsx @@ -17,8 +17,11 @@ import { useLinkMetadata, useListCommunities, useSearch, + useSoftware, useUploadImage, } from "../lib/api"; +import { supportsPollCreation } from "../lib/api/adapters/support"; +import { Forms } from "../lib/api/adapters/api-blueprint"; import _ from "lodash"; import { IonButton, @@ -287,6 +290,46 @@ export function CreatePost() { isEdit && post?.data.creatorApId && myUserId === post.data.creatorApId; const postOwner = post?.data.creatorSlug; + const softwareInfo = useSoftware(); + const { software } = softwareInfo; + const showPollOption = + supportsPollCreation(softwareInfo) || draft.type === "poll"; + + const DEFAULT_POLL: Forms.PollInput = { + endDate: dayjs().add(7, "days").toISOString(), + mode: "single", + localOnly: false, + choices: [ + { id: 0, text: "", sortOrder: 0 }, + { id: 0, text: "", sortOrder: 1 }, + ], + }; + + const patchPollChoice = (index: number, text: string) => { + if (!draft.poll) return; + const choices = draft.poll.choices.map((c, i) => + i === index ? { ...c, text } : c, + ); + patchDraft(draftId, { poll: { ...draft.poll, choices } }); + }; + + const addPollChoice = () => { + if (!draft.poll) return; + const choices = [ + ...draft.poll.choices, + { id: 0, text: "", sortOrder: draft.poll.choices.length }, + ]; + patchDraft(draftId, { poll: { ...draft.poll, choices } }); + }; + + const removePollChoice = (index: number) => { + if (!draft.poll) return; + const choices = draft.poll.choices + .filter((_, i) => i !== index) + .map((c, i) => ({ ...c, sortOrder: i })); + patchDraft(draftId, { poll: { ...draft.poll, choices } }); + }; + const uploadImage = useUploadImage(); const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { @@ -359,7 +402,11 @@ export function CreatePost() { // TODO: handle incomplete post data } }} - disabled={!draft.communitySlug || (isEdit && !canEdit)} + disabled={ + !draft.communitySlug || + (isEdit && !canEdit) || + (draft.type === "poll" && software === "lemmy") + } loading={ isEdit ? editPost.isPending || editPost.isSuccess @@ -441,17 +488,31 @@ export function CreatePost() { value={draft.type} onValueChange={(val) => { if (val) { - patchDraft(draftId, { - type: val as "text" | "media" | "link", - }); + const patch: Partial = { + type: val as Draft["type"], + }; + if (val === "poll" && !draft.poll) { + patch.poll = DEFAULT_POLL; + } + patchDraft(draftId, patch); } }} > Text Image Link + {showPollOption && ( + Poll + )} + {draft.type === "poll" && software === "lemmy" && ( +

+ Lemmy doesn't support polls. Switch to a different post type + or use a PieFed account. +

+ )} +
)} + {draft.type === "poll" && ( +
+
+ + {draft.poll?.choices.map((choice, i) => ( +
+ patchPollChoice(i, e.target.value)} + /> + {(draft.poll?.choices.length ?? 0) > 2 && ( + + )} +
+ ))} + +
+ +
+ + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + endDate: new Date(e.target.value).toISOString(), + }, + }) + } + /> +
+ +
+ + + val && + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + mode: val as "single" | "multiple", + }, + }) + } + > + + Single choice + + + Multiple choice + + +
+ +
+ + draft.poll && + patchDraft(draftId, { + poll: { ...draft.poll, localOnly: !!v }, + }) + } + /> + +
+
+ )} +
{ apId: string; flairs?: Pick[]; + poll?: PollInput; } export interface CreatePost @@ -606,6 +620,7 @@ export namespace Forms { | "nsfw" > { flairs?: Pick[]; + poll?: PollInput; } export type CreatePostReport = { diff --git a/src/lib/api/adapters/piefed.ts b/src/lib/api/adapters/piefed.ts index 16686f08..e6c83f21 100644 --- a/src/lib/api/adapters/piefed.ts +++ b/src/lib/api/adapters/piefed.ts @@ -1765,9 +1765,24 @@ export class PieFedApi implements ApiBlueprint { const res = await this.put("/post", { post_id, title: form.title, - url: form.url, + url: form.poll ? undefined : form.url, body: form.body, nsfw: form.nsfw ?? false, + ...(form.poll + ? { + poll: { + end_poll: form.poll.endDate, + mode: form.poll.mode, + local_only: form.poll.localOnly, + choices: form.poll.choices.map((c) => ({ + id: c.id, + choice_text: c.text, + sort_order: c.sortOrder, + num_votes: 0, + })), + }, + } + : {}), }); try { const data = z.object({ post_view: pieFedPostViewSchema }).parse(res); @@ -1805,6 +1820,21 @@ export class PieFedApi implements ApiBlueprint { url: form.url ?? undefined, body: form.body ?? undefined, nsfw: form.nsfw ?? false, + ...(form.poll + ? { + poll: { + end_poll: form.poll.endDate, + mode: form.poll.mode, + local_only: form.poll.localOnly, + choices: form.poll.choices.map((c) => ({ + id: c.id, + choice_text: c.text, + sort_order: c.sortOrder, + num_votes: 0, + })), + }, + } + : {}), }); try { const data = z.object({ post_view: pieFedPostViewSchema }).parse(res); diff --git a/src/lib/api/adapters/support.ts b/src/lib/api/adapters/support.ts index d61be22c..65b87509 100644 --- a/src/lib/api/adapters/support.ts +++ b/src/lib/api/adapters/support.ts @@ -25,3 +25,7 @@ export function supportsFeeds({ software, softwareVersion }: Software) { export function supportsMarkCommentAsAnswer({ software }: Software) { return software === "piefed"; } + +export function supportsPollCreation({ software }: Software) { + return software === "piefed"; +} diff --git a/src/stores/create-post.ts b/src/stores/create-post.ts index 38339b5b..0eb817bb 100644 --- a/src/stores/create-post.ts +++ b/src/stores/create-post.ts @@ -14,7 +14,7 @@ export type CommunityPartial = Pick< >; export interface Draft extends Partial { - type: "text" | "media" | "link"; + type: "text" | "media" | "link" | "poll"; createdAt: number; } @@ -60,12 +60,30 @@ export function postToDraft( body: post.body ?? "", communitySlug: post.communitySlug, createdAt: dayjs(post.createdAt).toDate().valueOf(), - type: post.url ? "link" : post.thumbnailUrl ? "media" : "text", + type: post.url + ? "link" + : post.thumbnailUrl + ? "media" + : post.poll + ? "poll" + : "text", apId: post.apId, thumbnailUrl: post.thumbnailUrl, altText: post.altText, url: post.url, flairs: flairs ?? undefined, + poll: post.poll + ? { + endDate: post.poll.endDate, + mode: post.poll.mode, + localOnly: post.poll.localOnly, + choices: post.poll.choices.map((c, i) => ({ + id: c.id, + text: c.text, + sortOrder: i, + })), + } + : undefined, }; } @@ -99,6 +117,10 @@ export function draftToEditPostData(draft: Draft): Forms.EditPost { case "media": post.url = post.thumbnailUrl; break; + case "poll": + post.url = null; + post.thumbnailUrl = null; + break; case "link": } @@ -139,6 +161,21 @@ export function draftToCreatePostData(draft: Draft): Forms.CreatePost { case "media": post.url = post.thumbnailUrl; break; + case "poll": + post.url = null; + post.thumbnailUrl = null; + if (post.poll) { + let maxId = Math.max(0, ...post.poll.choices.map((c) => c.id)); + post.poll = { + ...post.poll, + choices: post.poll.choices.map((c, i) => ({ + ...c, + id: c.id ?? ++maxId, + sortOrder: i, + })), + }; + } + break; case "link": } From 99a251a51889e083a350a2e23d09a610ccc9577c Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:58:56 -0500 Subject: [PATCH 02/12] refactor: cleanup create poll post ui --- src/components/markdown/editor.tsx | 4 + src/features/create-post.tsx | 116 ++++++++++++++++------------- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/src/components/markdown/editor.tsx b/src/components/markdown/editor.tsx index 1fd0a4f1..ca43befc 100644 --- a/src/components/markdown/editor.tsx +++ b/src/components/markdown/editor.tsx @@ -704,6 +704,7 @@ export function MarkdownEditor({ autoFocus: autoFocusDefault, placeholder, onFocus, + onBlur, onChageEditorType, footer, id, @@ -715,6 +716,7 @@ export function MarkdownEditor({ autoFocus?: boolean; placeholder?: string; onFocus?: () => void; + onBlur?: () => void; onChageEditorType?: () => void; footer?: React.ReactNode; id?: string; @@ -738,6 +740,7 @@ export function MarkdownEditor({ autoFocus={autoFocus} placeholder={placeholder} onFocus={onFocus} + onBlur={onBlur} id={id} hideMenu={hideMenu} /> @@ -753,6 +756,7 @@ export function MarkdownEditor({ autoFocus={autoFocus} placeholder={placeholder} onFocus={onFocus} + onBlur={onBlur} id={id} hideMenu={hideMenu} /> diff --git a/src/features/create-post.tsx b/src/features/create-post.tsx index 1192e293..738b19ff 100644 --- a/src/features/create-post.tsx +++ b/src/features/create-post.tsx @@ -360,6 +360,8 @@ export function CreatePost() { resetEditPost(); }, [draftId, resetCreatePost, resetEditPost]); + const [editingBody, setEditingBody] = useState(false); + const linkMetadata = useLinkMetadata(); const parseUrl = (url: string) => { @@ -513,17 +515,34 @@ export function CreatePost() {

)} -
- - patchDraft(draftId, { - nsfw: nsfw === true, - }) - } - /> - +
+
+ + patchDraft(draftId, { + nsfw: nsfw === true, + }) + } + /> + +
+ + {draft.type === "poll" && ( +
+ + draft.poll && + patchDraft(draftId, { + poll: { ...draft.poll, localOnly: !!v }, + }) + } + /> + +
+ )}
{flairs && flairs.length > 0 && ( @@ -601,6 +620,38 @@ export function CreatePost() {
)} +
+ + + patchDraft(draftId, { + title: e.currentTarget.value ?? "", + }) + } + /> +
+ +
+ + + patchDraft(draftId, { + body, + }) + } + className="md:border md:rounded-lg md:shadow-xs max-md:-mx-3.5 max-md:flex-1" + placeholder="Write something..." + onFocus={() => setEditingBody(true)} + onBlur={() => setEditingBody(false)} + hideMenu={!editingBody} + /> +
+ {draft.type === "poll" && (
@@ -680,51 +731,10 @@ export function CreatePost() {
- -
- - draft.poll && - patchDraft(draftId, { - poll: { ...draft.poll, localOnly: !!v }, - }) - } - /> - -
)} -
- - - patchDraft(draftId, { - title: e.currentTarget.value ?? "", - }) - } - /> -
- -
- - - patchDraft(draftId, { - body, - }) - } - className="md:border md:rounded-lg md:shadow-xs max-md:-mx-3.5 max-md:flex-1" - placeholder="Write something..." - /> - {getPostButton("self-end max-md:hidden")} -
+ {getPostButton("self-end max-md:hidden")}
)} From 8e69ea7b84c6cfa56361bd8cf44e0addfd9329fc Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:09:25 -0500 Subject: [PATCH 03/12] refactor: cleanup poll create ui --- src/components/icons.tsx | 1 + src/components/ui/input.tsx | 2 +- src/components/ui/simple-select.tsx | 48 +++++++++ src/features/create-post.tsx | 137 +++++++++++++------------- src/lib/api/adapters/api-blueprint.ts | 2 +- src/lib/api/adapters/piefed.ts | 9 +- src/stores/create-post.ts | 2 +- 7 files changed, 124 insertions(+), 77 deletions(-) create mode 100644 src/components/ui/simple-select.tsx diff --git a/src/components/icons.tsx b/src/components/icons.tsx index b815b6c5..57d89122 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -23,6 +23,7 @@ export { FaRobot as Robot, FaLock as Lock, FaCheck as Check, + FaTrash as Trash, } from "react-icons/fa6"; export { PiShareFat as Share } from "react-icons/pi"; export { CgSearch as Search, CgSpinner as Spinner } from "react-icons/cg"; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index c8839e1d..31216c01 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -46,7 +46,7 @@ function Input({ /> {endAdornment && ( -
+
{endAdornment}
)} diff --git a/src/components/ui/simple-select.tsx b/src/components/ui/simple-select.tsx new file mode 100644 index 00000000..985f0ce8 --- /dev/null +++ b/src/components/ui/simple-select.tsx @@ -0,0 +1,48 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/src/components/ui/select"; + +interface SimpleSelectProps { + options: O[]; + value: O | undefined; + onChange: (value: O) => void; + valueGetter: (option: O) => string; + labelGetter: (option: O) => string; + placeholder?: string; + className?: string; +} + +export function SimpleSelect({ + options, + value, + onChange, + valueGetter, + labelGetter, + placeholder, + className, +}: SimpleSelectProps) { + return ( + + ); +} diff --git a/src/features/create-post.tsx b/src/features/create-post.tsx index 738b19ff..6bc3046c 100644 --- a/src/features/create-post.tsx +++ b/src/features/create-post.tsx @@ -1,6 +1,6 @@ import { ContentGutters } from "../components/gutters"; import { useRecentCommunitiesStore } from "../stores/recent-communities"; -import { useCallback, useEffect, useId, useRef, useState } from "react"; +import { useCallback, useEffect, useId, useState } from "react"; import { Draft, isEmptyDraft, @@ -69,6 +69,8 @@ import { Flair } from "../components/flair"; import { Checkbox } from "@/src/components/ui/checkbox"; import { useFlairs } from "../stores/flairs"; import { Page } from "../components/page"; +import { SimpleSelect } from "../components/ui/simple-select"; +import { Trash } from "../components/icons"; dayjs.extend(localizedFormat); @@ -296,7 +298,7 @@ export function CreatePost() { supportsPollCreation(softwareInfo) || draft.type === "poll"; const DEFAULT_POLL: Forms.PollInput = { - endDate: dayjs().add(7, "days").toISOString(), + endDays: 7, mode: "single", localOnly: false, choices: [ @@ -540,7 +542,7 @@ export function CreatePost() { }) } /> - +
)}
@@ -648,90 +650,83 @@ export function CreatePost() { placeholder="Write something..." onFocus={() => setEditingBody(true)} onBlur={() => setEditingBody(false)} - hideMenu={!editingBody} + hideMenu={!editingBody && draft.type !== "text"} /> {draft.type === "poll" && ( -
-
+ <> +
{draft.poll?.choices.map((choice, i) => ( -
- patchPollChoice(i, e.target.value)} - /> - {(draft.poll?.choices.length ?? 0) > 2 && ( - - )} -
+ patchPollChoice(i, e.target.value)} + wrapperClassName="rounded-none first-of-type:rounded-t-lg h-10" + endAdornment={ + (draft.poll?.choices.length ?? 0) > 2 && ( + + ) + } + /> ))}
-
- - - draft.poll && - patchDraft(draftId, { - poll: { - ...draft.poll, - endDate: new Date(e.target.value).toISOString(), - }, - }) - } - /> -
- -
- - - val && - draft.poll && - patchDraft(draftId, { - poll: { - ...draft.poll, - mode: val as "single" | "multiple", - }, - }) - } - > - - Single choice - - - Multiple choice - - +
+
+ + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + mode: mode, + }, + }) + } + valueGetter={(opt) => opt} + labelGetter={(opt) => _.capitalize(opt)} + /> +
+ +
+ + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + endDays: parseFloat(e.target.value), + }, + }) + } + /> +
-
+ )} {getPostButton("self-end max-md:hidden")} diff --git a/src/lib/api/adapters/api-blueprint.ts b/src/lib/api/adapters/api-blueprint.ts index 97c307dc..03291633 100644 --- a/src/lib/api/adapters/api-blueprint.ts +++ b/src/lib/api/adapters/api-blueprint.ts @@ -592,7 +592,7 @@ export namespace Forms { } export interface PollInput { - endDate: string; // ISO datetime — matches postPollSchema.endDate + endDays: number; // days from now until the poll expires mode: "single" | "multiple"; // matches postPollSchema.mode localOnly: boolean; // matches postPollSchema.localOnly choices: PollChoiceInput[]; diff --git a/src/lib/api/adapters/piefed.ts b/src/lib/api/adapters/piefed.ts index e6c83f21..d63cb006 100644 --- a/src/lib/api/adapters/piefed.ts +++ b/src/lib/api/adapters/piefed.ts @@ -1771,14 +1771,15 @@ export class PieFedApi implements ApiBlueprint { ...(form.poll ? { poll: { - end_poll: form.poll.endDate, + end_poll: new Date( + Date.now() + form.poll.endDays * 24 * 60 * 60 * 1000, + ).toISOString(), mode: form.poll.mode, local_only: form.poll.localOnly, choices: form.poll.choices.map((c) => ({ id: c.id, choice_text: c.text, sort_order: c.sortOrder, - num_votes: 0, })), }, } @@ -1823,7 +1824,9 @@ export class PieFedApi implements ApiBlueprint { ...(form.poll ? { poll: { - end_poll: form.poll.endDate, + end_poll: new Date( + Date.now() + form.poll.endDays * 24 * 60 * 60 * 1000, + ).toISOString(), mode: form.poll.mode, local_only: form.poll.localOnly, choices: form.poll.choices.map((c) => ({ diff --git a/src/stores/create-post.ts b/src/stores/create-post.ts index 0eb817bb..4fd46780 100644 --- a/src/stores/create-post.ts +++ b/src/stores/create-post.ts @@ -74,7 +74,7 @@ export function postToDraft( flairs: flairs ?? undefined, poll: post.poll ? { - endDate: post.poll.endDate, + endDays: Math.max(1, dayjs(post.poll.endDate).diff(dayjs(), "day")), mode: post.poll.mode, localOnly: post.poll.localOnly, choices: post.poll.choices.map((c, i) => ({ From 3650575033e9582d0925a98866288d9ab9c0be9a Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:34:35 -0500 Subject: [PATCH 04/12] refactor: add poll end date unit select --- src/features/create-post.tsx | 74 ++++++++++++++++++++------- src/lib/api/adapters/api-blueprint.ts | 3 +- src/lib/api/adapters/piefed.ts | 27 +++++++--- src/stores/create-post.ts | 3 +- 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/features/create-post.tsx b/src/features/create-post.tsx index 6bc3046c..c21506eb 100644 --- a/src/features/create-post.tsx +++ b/src/features/create-post.tsx @@ -71,9 +71,22 @@ import { useFlairs } from "../stores/flairs"; import { Page } from "../components/page"; import { SimpleSelect } from "../components/ui/simple-select"; import { Trash } from "../components/icons"; +import { Separator } from "../components/ui/separator"; dayjs.extend(localizedFormat); +const POLL_UNIT_OPTIONS: { + value: Forms.PollInput["endUnit"]; + label: string; +}[] = [ + { value: "minutes", label: "Minutes" }, + { value: "hours", label: "Hours" }, + { value: "days", label: "Days" }, + { value: "weeks", label: "Weeks" }, + { value: "months", label: "Months" }, + { value: "permanent", label: "Permanent" }, +]; + const EMPTY_ARR: never[] = []; function DraftsSidebar({ @@ -298,7 +311,8 @@ export function CreatePost() { supportsPollCreation(softwareInfo) || draft.type === "poll"; const DEFAULT_POLL: Forms.PollInput = { - endDays: 7, + endAmount: 7, + endUnit: "days", mode: "single", localOnly: false, choices: [ @@ -651,6 +665,7 @@ export function CreatePost() { onFocus={() => setEditingBody(true)} onBlur={() => setEditingBody(false)} hideMenu={!editingBody && draft.type !== "text"} + onChageEditorType={() => setEditingBody(true)} />
@@ -664,7 +679,7 @@ export function CreatePost() { placeholder={`Option ${i + 1}`} value={choice.text} onChange={(e) => patchPollChoice(i, e.target.value)} - wrapperClassName="rounded-none first-of-type:rounded-t-lg h-10" + wrapperClassName="rounded-none first-of-type:rounded-t-lg h-10 pr-0.5" endAdornment={ (draft.poll?.choices.length ?? 0) > 2 && (
-
+
+ +
- - - draft.poll && - patchDraft(draftId, { - poll: { - ...draft.poll, - endDays: parseFloat(e.target.value), - }, - }) - } - /> + +
+ {draft.poll?.endUnit !== "permanent" && ( + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + endAmount: parseFloat(e.target.value), + }, + }) + } + /> + )} + o.value === (draft.poll?.endUnit ?? "days"), + )} + onChange={(o) => + draft.poll && + patchDraft(draftId, { + poll: { ...draft.poll, endUnit: o.value }, + }) + } + valueGetter={(o) => o.value} + labelGetter={(o) => o.label} + /> +
diff --git a/src/lib/api/adapters/api-blueprint.ts b/src/lib/api/adapters/api-blueprint.ts index 03291633..9cad6509 100644 --- a/src/lib/api/adapters/api-blueprint.ts +++ b/src/lib/api/adapters/api-blueprint.ts @@ -592,7 +592,8 @@ export namespace Forms { } export interface PollInput { - endDays: number; // days from now until the poll expires + endAmount: number; + endUnit: "minutes" | "hours" | "days" | "weeks" | "months" | "permanent"; mode: "single" | "multiple"; // matches postPollSchema.mode localOnly: boolean; // matches postPollSchema.localOnly choices: PollChoiceInput[]; diff --git a/src/lib/api/adapters/piefed.ts b/src/lib/api/adapters/piefed.ts index d63cb006..916f5056 100644 --- a/src/lib/api/adapters/piefed.ts +++ b/src/lib/api/adapters/piefed.ts @@ -796,6 +796,25 @@ export function flattenCommentViews( return result; } +const POLL_UNIT_MS: Record = { + minutes: 60 * 1000, + hours: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000, + weeks: 7 * 24 * 60 * 60 * 1000, + months: 30 * 24 * 60 * 60 * 1000, +}; + +function pollEndDate(poll: Forms.PollInput): string { + if (poll.endUnit === "permanent") { + return new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString(); + } + return new Date( + Date.now() + + poll.endAmount * + (POLL_UNIT_MS[poll.endUnit] ?? POLL_UNIT_MS["days"] ?? 0), + ).toISOString(); +} + export class PieFedApi implements ApiBlueprint { software = Software.PIEFED; softwareVersion: string; @@ -1771,9 +1790,7 @@ export class PieFedApi implements ApiBlueprint { ...(form.poll ? { poll: { - end_poll: new Date( - Date.now() + form.poll.endDays * 24 * 60 * 60 * 1000, - ).toISOString(), + end_poll: pollEndDate(form.poll), mode: form.poll.mode, local_only: form.poll.localOnly, choices: form.poll.choices.map((c) => ({ @@ -1824,9 +1841,7 @@ export class PieFedApi implements ApiBlueprint { ...(form.poll ? { poll: { - end_poll: new Date( - Date.now() + form.poll.endDays * 24 * 60 * 60 * 1000, - ).toISOString(), + end_poll: pollEndDate(form.poll), mode: form.poll.mode, local_only: form.poll.localOnly, choices: form.poll.choices.map((c) => ({ diff --git a/src/stores/create-post.ts b/src/stores/create-post.ts index 4fd46780..41377988 100644 --- a/src/stores/create-post.ts +++ b/src/stores/create-post.ts @@ -74,7 +74,8 @@ export function postToDraft( flairs: flairs ?? undefined, poll: post.poll ? { - endDays: Math.max(1, dayjs(post.poll.endDate).diff(dayjs(), "day")), + endAmount: Math.max(1, dayjs(post.poll.endDate).diff(dayjs(), "day")), + endUnit: "days", mode: post.poll.mode, localOnly: post.poll.localOnly, choices: post.poll.choices.map((c, i) => ({ From a33063997fbf1973bd82b66904641fe48df09f2d Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:43:21 -0500 Subject: [PATCH 05/12] fix: markdown editor buttons firing blur --- src/components/markdown/editor.tsx | 4 ++++ src/features/create-post.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/markdown/editor.tsx b/src/components/markdown/editor.tsx index ca43befc..ded95d0f 100644 --- a/src/components/markdown/editor.tsx +++ b/src/components/markdown/editor.tsx @@ -515,6 +515,7 @@ function TipTapEditor({ "flex flex-row justify-between py-1.5 px-2 pb-0 max-md:hidden", hideMenu && "hidden", )} + onMouseDown={(e) => e.preventDefault()} > e.preventDefault()} > e.preventDefault()} >
From 9c036d0f95952dc11fab154338404135b8160f75 Mon Sep 17 00:00:00 2001 From: Christian <5768801+christianjuth@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:59:40 -0500 Subject: [PATCH 06/12] refactor: reduce emphasis on post editor flair --- src/components/ui/multi-select.tsx | 12 ++++++++-- src/features/create-post.tsx | 36 ++++++++++++++++-------------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/components/ui/multi-select.tsx b/src/components/ui/multi-select.tsx index 92a1560d..c6aa5a1a 100644 --- a/src/components/ui/multi-select.tsx +++ b/src/components/ui/multi-select.tsx @@ -6,8 +6,9 @@ import { DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; import { ChevronDown } from "lucide-react"; -import { isNotNil } from "@/src/lib/utils"; +import { cn, isNotNil } from "@/src/lib/utils"; import { Fragment } from "react/jsx-runtime"; +import { ComponentProps } from "react"; interface Option { value: V; @@ -21,6 +22,8 @@ interface MultiSelectProps { placeholder?: string; renderOption: (opt: Option) => React.ReactNode; keyExtractor: (opt: V) => string | number; + buttonVariant?: ComponentProps["variant"]; + buttonClassName?: string; } export function MultiSelect({ @@ -30,6 +33,8 @@ export function MultiSelect({ placeholder, renderOption, keyExtractor, + buttonVariant = "outline", + buttonClassName, }: MultiSelectProps) { const selected = value .map((value) => @@ -40,7 +45,10 @@ export function MultiSelect({ return ( -