From b8a976b6e986bf8d3db1744c4c6df4ccf48cf3c2 Mon Sep 17 00:00:00 2001 From: Josh Rouwhorst Date: Thu, 16 Oct 2025 00:45:39 -0400 Subject: [PATCH 1/4] Overhauled header nav --- src/components/HeaderNav.tsx | 73 +++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/components/HeaderNav.tsx b/src/components/HeaderNav.tsx index 627b0b7..ce908fb 100644 --- a/src/components/HeaderNav.tsx +++ b/src/components/HeaderNav.tsx @@ -1,29 +1,50 @@ -import { HEADER_NAV_ITEMS } from "@/config/frontend"; -import Link from "next/link"; +'use client' +import { HEADER_NAV_ITEMS } from '@/config/frontend' +import Link from 'next/link' +import { Menu } from 'lucide-react' +import { useState } from 'react' +import { Button } from './ui/forms' -const HeaderNav = () => ( -
- -
- - Local backup, drafts & scheduling for Bluesky. - -

BskyBackup

+const HeaderNav = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false) + + return ( +
+
+
BskyBackup
+
+ + {/* Nav: hidden on lg and below unless menu is open */} + + {/* Hamburger button for mobile */} +
-
-); + ) +} -export default HeaderNav; +export default HeaderNav From 6af1ef23f60814b9879a44629470da95f12cb3f0 Mon Sep 17 00:00:00 2001 From: Josh Rouwhorst Date: Thu, 16 Oct 2025 14:01:19 -0400 Subject: [PATCH 2/4] updating mobile support --- package-lock.json | 10 + package.json | 1 + src/app/drafts/page.tsx | 13 +- src/app/page.tsx | 13 +- src/app/schedules/page.tsx | 76 +++----- src/components/HeaderNav.tsx | 40 ++-- src/components/schedules/FrequencyInput.tsx | 4 +- src/components/schedules/ScheduleDetails.tsx | 9 +- src/components/schedules/ScheduleEditForm.tsx | 67 ++++--- src/components/schedules/ScheduleList.tsx | 9 +- src/components/ui/TwoColumn.tsx | 184 ++++++++++++++++++ 11 files changed, 314 insertions(+), 112 deletions(-) create mode 100644 src/components/ui/TwoColumn.tsx diff --git a/package-lock.json b/package-lock.json index e548439..18f1116 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@atproto/api": "^0.16.9", "@tailwindcss/postcss": "^4.1.13", + "clsx": "^2.1.1", "dayjs": "^1.11.18", "exifreader": "^4.32.0", "jimp": "^1.6.0", @@ -3596,6 +3597,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", diff --git a/package.json b/package.json index 30ee24e..b5563bd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@atproto/api": "^0.16.9", "@tailwindcss/postcss": "^4.1.13", + "clsx": "^2.1.1", "dayjs": "^1.11.18", "exifreader": "^4.32.0", "jimp": "^1.6.0", diff --git a/src/app/drafts/page.tsx b/src/app/drafts/page.tsx index dcc1fe6..844887e 100644 --- a/src/app/drafts/page.tsx +++ b/src/app/drafts/page.tsx @@ -4,16 +4,17 @@ import { Callout } from '@/components/ui/callout' import { LinkButton } from '@/components/ui/forms' import DraftsProvider from '@/providers/DraftsProvider' import { Plus } from 'lucide-react' +import TwoColumn from '@/components/ui/TwoColumn' export default async function Drafts() { return ( -
-
+ + -
- -
+ +
) } diff --git a/src/app/page.tsx b/src/app/page.tsx index bc97707..516ad2b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,17 +5,18 @@ import BskyBackupProvider from '@/providers/BskyBackupProvider' import DraftProvider from '@/providers/DraftsProvider' import Link from '@/components/ui/link' import { Callout } from '@/components/ui/callout' +import TwoColumn from '@/components/ui/TwoColumn' export default async function Home() { return ( -
-
+ + -
- -
+ +
) diff --git a/src/app/schedules/page.tsx b/src/app/schedules/page.tsx index 3aecb92..7b0b84d 100644 --- a/src/app/schedules/page.tsx +++ b/src/app/schedules/page.tsx @@ -16,6 +16,10 @@ export default function SchedulesPage() { const [isEditing, setIsEditing] = useState(false) const [editForm, setEditForm] = useState>({}) + const handleCancel = () => { + setIsEditing(false) + } + const handleEdit = (schedule: Schedule) => { setSelectedSchedule(schedule) setEditForm(schedule) @@ -42,9 +46,9 @@ export default function SchedulesPage() { -
-
- {/* Schedule List */} +
+ {/* Schedule List */} + {!selectedSchedule && !isEditing && ( + )} + + {selectedSchedule && !isEditing && ( + + setSelectedSchedule(null)} + /> + + )} - {/* Edit Panel */} -
-
-

- {isEditing - ? selectedSchedule - ? 'Edit Schedule' - : 'Create Schedule' - : 'Schedule Details'} -

-
-
- {selectedSchedule || isEditing ? ( -
- {isEditing ? ( - - - - ) : ( - - - - )} -
- ) : ( -

- Select a schedule to view details -

- )} -
-
-
+ {/* Edit Panel */} + {isEditing && ( + + + + )}
diff --git a/src/components/HeaderNav.tsx b/src/components/HeaderNav.tsx index ce908fb..f046082 100644 --- a/src/components/HeaderNav.tsx +++ b/src/components/HeaderNav.tsx @@ -4,29 +4,39 @@ import Link from 'next/link' import { Menu } from 'lucide-react' import { useState } from 'react' import { Button } from './ui/forms' +import { clsx } from 'clsx' + +const styles = { + navBar: + 'relative flex flex-row items-stretch justify-between border-b border-gray-200 dark:border-gray-700', + logoContainer: + 'flex items-center p-4 border-l border-gray-200 bg-blue-200 text-blue-950 dark:border-gray-700 dark:bg-blue-950 dark:text-white', + logoText: 'text-2xl font-bold', + navMenu: + 'absolute top-full right-0 lg:relative lg:top-auto lg:right-auto lg:block', + navList: + 'list-none h-full border-gray-200 border-1 flex flex-col px-10 py-5 bg-[var(--background)] dark:border-gray-800 lg:border-0 lg:items-center lg:flex-row lg:gap-4 lg:m-0 lg:p-0', + navItem: 'p-4 lg:inline-block', + navLink: + 'p-4 rounded text-gray-700 hover:text-blue-600 active:text-blue-900 active:bg-blue-500 transition-colors dark:text-gray-200 dark:hover:text-blue-400 dark:active:text-blue-200', + hamburgerButton: 'flex items-center px-4 lg:hidden', +} const HeaderNav = () => { const [isMenuOpen, setIsMenuOpen] = useState(false) return ( -
-
-
BskyBackup
+
+
+
BskyBackup
{/* Nav: hidden on lg and below unless menu is open */} -
) } diff --git a/src/components/schedules/ScheduleEditForm.tsx b/src/components/schedules/ScheduleEditForm.tsx index 38b98b1..2e6cb2f 100644 --- a/src/components/schedules/ScheduleEditForm.tsx +++ b/src/components/schedules/ScheduleEditForm.tsx @@ -1,30 +1,29 @@ -import React from "react"; -import { Schedule, CreateScheduleRequest } from "@/types/scheduler"; -import { useScheduleContext } from "@/providers/ScheduleProvider"; -import FrequencyInput from "./FrequencyInput"; -import { Input, Label, Checkbox, Button } from "../ui/forms"; -import TimezoneSelect from "./TimezoneSelect"; -import { GroupSelect } from "../GroupSelect"; +import React from 'react' +import { Schedule, CreateScheduleRequest } from '@/types/scheduler' +import { useScheduleContext } from '@/providers/ScheduleProvider' +import FrequencyInput from './FrequencyInput' +import { Input, Label, Checkbox, Button } from '../ui/forms' +import { GroupSelect } from '../GroupSelect' export default function ScheduleEditForm({ schedule, editForm, setEditForm, onSave, - setIsEditing, + onCancel, }: { - schedule: Schedule | null; - editForm: Partial; - setEditForm: React.Dispatch>>; - onSave: () => void; - setIsEditing: React.Dispatch>; + schedule: Schedule | null + editForm: Partial + setEditForm: React.Dispatch>> + onSave: () => void + onCancel: React.Dispatch> }) { - const { createSchedule, updateSchedule } = useScheduleContext(); + const { createSchedule, updateSchedule } = useScheduleContext() const handleSave = async () => { if (!editForm.name || !editForm.frequency) { - alert("Name and Frequency are required"); - return; + alert('Name and Frequency are required') + return } if (schedule && editForm.id) { @@ -35,31 +34,31 @@ export default function ScheduleEditForm({ isActive: editForm.isActive || false, frequency: editForm.frequency, platforms: editForm.platforms || [], - group: editForm.group || "default", - }; - await updateSchedule(updatedSchedule); + group: editForm.group || 'default', + } + await updateSchedule(updatedSchedule) } else { // Create new schedule const newSchedule: CreateScheduleRequest = { - name: editForm.name || "", + name: editForm.name || '', isActive: editForm.isActive || false, frequency: editForm.frequency, platforms: editForm.platforms || [], - group: editForm.group || "default", - }; - await createSchedule(newSchedule); + group: editForm.group || 'default', + } + await createSchedule(newSchedule) } - await onSave(); - }; + await onSave() + } return ( - <> +
setEditForm((prev) => ({ ...prev, @@ -78,19 +77,19 @@ export default function ScheduleEditForm({ setEditForm((prev) => ({ ...prev, group, - })); + })) }} />
-
+
{ setEditForm((prev) => ({ ...prev, frequency, - })); + })) }} />
@@ -116,8 +115,8 @@ export default function ScheduleEditForm({
- - ); +
+ ) } diff --git a/src/components/schedules/ScheduleList.tsx b/src/components/schedules/ScheduleList.tsx index 51ee85f..28f13d8 100644 --- a/src/components/schedules/ScheduleList.tsx +++ b/src/components/schedules/ScheduleList.tsx @@ -20,7 +20,7 @@ export default function ScheduleList({ }) { const { schedules } = useScheduleContext() return ( -
+ <>

@@ -36,6 +36,11 @@ export default function ScheduleList({

+ {schedules.length === 0 && ( +
+ No schedules found. +
+ )} {schedules .sort((a, b) => a.name.localeCompare(b.name)) .map((schedule) => ( @@ -65,6 +70,6 @@ export default function ScheduleList({ ))}
-
+ ) } diff --git a/src/components/ui/TwoColumn.tsx b/src/components/ui/TwoColumn.tsx new file mode 100644 index 0000000..1a7154c --- /dev/null +++ b/src/components/ui/TwoColumn.tsx @@ -0,0 +1,184 @@ +/** biome-ignore-all lint/suspicious/noArrayIndexKey: Need it for columns */ +import clsx from 'clsx' +import { cloneElement } from 'react' + +interface TwoColumnProps { + reverseStack?: boolean + stackPoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' + children?: React.ReactNode +} + +interface ColumnProps { + children?: React.ReactNode + stackPoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' + className?: string +} + +export default function TwoColumn({ + reverseStack = false, + stackPoint = 'md', + children, +}: TwoColumnProps) { + const setChildProps = (children: React.ReactNode) => { + if (!children) return + if (!Array.isArray(children)) return children + + return children.map((child, index) => { + if ( + child && + typeof child === 'object' && + 'type' in child && + ((child as any).type === Main || + (child as any).type === Side || + (child as any).type === Column) + ) { + const clone = cloneElement(child, { + stackPoint, + key: `two-column-child-${index}`, + }) + return clone + } + return child + }) + } + + let stackClasses = null + switch (stackPoint) { + case 'xs': + stackClasses = 'w-full' + break + case 'sm': + stackClasses = 'sm:flex-row' + break + case 'md': + stackClasses = 'md:flex-row' + break + case 'lg': + stackClasses = 'lg:flex-row' + break + case 'xl': + stackClasses = 'xl:flex-row' + break + case '2xl': + stackClasses = '2xl:flex-row' + break + default: + stackClasses = 'md:flex-row' + break + } + + return ( +
+ {setChildProps(children)} +
+ ) +} + +function Column({ children, stackPoint = 'md', className }: ColumnProps) { + let stackClasses = null + switch (stackPoint) { + case 'xs': + stackClasses = 'w-full' + break + case 'sm': + stackClasses = 'sm:w-1/2' + break + case 'md': + stackClasses = 'md:w-1/2' + break + case 'lg': + stackClasses = 'lg:w-1/2' + break + case 'xl': + stackClasses = 'xl:w-1/2' + break + case '2xl': + stackClasses = '2xl:w-1/2' + break + default: + stackClasses = 'md:w-1/2' + break + } + + return ( +
+ {children} +
+ ) +} + +function Main({ children, stackPoint = 'md', className }: ColumnProps) { + let stackClasses = null + switch (stackPoint) { + case 'xs': + stackClasses = 'w-full' + break + case 'sm': + stackClasses = 'sm:w-2/3 xl:w-1/2' + break + case 'md': + stackClasses = 'md:w-2/3 xl:w-1/2' + break + case 'lg': + stackClasses = 'lg:w-2/3 xl:w-1/2' + break + case 'xl': + stackClasses = 'xl:w-2/3 2xl:w-1/2' + break + case '2xl': + stackClasses = '2xl:w-2/3' + break + default: + stackClasses = 'md:w-2/3' + break + } + + return ( +
+ {children} +
+ ) +} + +function Side({ children, stackPoint = 'md', className }: ColumnProps) { + let stackClasses = null + switch (stackPoint) { + case 'xs': + stackClasses = 'w-full' + break + case 'sm': + stackClasses = 'sm:w-1/3 xl:w-1/2' + break + case 'md': + stackClasses = 'md:w-1/3 xl:w-1/2' + break + case 'lg': + stackClasses = 'lg:w-1/3 xl:w-1/2' + break + case 'xl': + stackClasses = 'xl:w-1/3 2xl:w-1/2' + break + case '2xl': + stackClasses = '2xl:w-1/3' + break + default: + stackClasses = 'md:w-1/3' + break + } + + return ( + + ) +} + +TwoColumn.Main = Main +TwoColumn.Side = Side +TwoColumn.Column = Column From 1bbebcbe7df4a783c206544efba88acf61f80057 Mon Sep 17 00:00:00 2001 From: Josh Rouwhorst Date: Thu, 16 Oct 2025 16:57:50 -0400 Subject: [PATCH 3/4] Settings and schedule details mobile ui setup --- src/app/settings/components/SettingsForm.tsx | 2 +- src/components/schedules/ScheduleDetails.tsx | 29 ++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/app/settings/components/SettingsForm.tsx b/src/app/settings/components/SettingsForm.tsx index 0061a73..51d0965 100644 --- a/src/app/settings/components/SettingsForm.tsx +++ b/src/app/settings/components/SettingsForm.tsx @@ -49,7 +49,7 @@ export default function SettingsForm() { onSubmit={handleSubmit} className="space-y-6 bg-color-gray-200 dark:bg-gray-900 p-6 rounded-lg" > -
+
- {lookups?.nextPost && ( )} {schedule.group && ( @@ -171,20 +182,20 @@ export default function ScheduleDetails({
{lookups && lookups.nextPostDates.length > 0 ? ( -
+
{formatFullDateTime(lookups.nextPostDates[0])}
) : ( -
No next post date available.
+
No next post date available.
)} {lookups?.nextPost ? ( -
+
) : ( -
No upcoming posts.
+
No upcoming posts.
)}
) From 3556fa373c513bdf24a78c04dcdc453bc1ef1817 Mon Sep 17 00:00:00 2001 From: Josh Rouwhorst Date: Thu, 16 Oct 2025 17:01:17 -0400 Subject: [PATCH 4/4] code cleanup --- src/components/schedules/ScheduleDetails.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/schedules/ScheduleDetails.tsx b/src/components/schedules/ScheduleDetails.tsx index 8ac98e3..1c7cfb2 100644 --- a/src/components/schedules/ScheduleDetails.tsx +++ b/src/components/schedules/ScheduleDetails.tsx @@ -4,13 +4,7 @@ import { Button, Label, LinkButton } from '../ui/forms' import { displayTime, formatFullDateTime } from '@/helpers/utils' import { useEffect, useState } from 'react' import Post from '../Post' -import { - ArrowLeftIcon, - PencilIcon, - TrashIcon, - ShareIcon, - Pen, -} from 'lucide-react' +import { ArrowLeftIcon, PencilIcon, TrashIcon, ShareIcon } from 'lucide-react' export default function ScheduleDetails({ schedule,