···102102103103### Minimization
104104105105-The application by default is configured to use the minified versions of the scripts in `assets/js`. You can either turn off this behavior in `src/limits.ts` or run `npm run minify` whenever those files change.
105105+The application by default is configured to use the minified versions of the scripts in `assets/js`. By default these rebuild whenever any typescript file is changed or the application is deployed/ran.
106106107107## Project Structure
108108···132132133133#### Server
134134135135-- Zod - data validation
136136-- BetterAuth - login/authentication
137137-- BetterAuthCloudflare - helper adaption
135135+- BetterAuth - site login/authentication
136136+- BetterAuthCloudflare - helper for BetterAuth on CF
138137- hono - request routing/processing
139138- uuid - id generation
139139+- zod - data validation
140140- image-dimensions - image data validation
141141- date-fns - date processing helpers
142142- drizzle - database orm/schemas
···148148- tribute - client autocomplete library
149149- toastify - client notifications
150150- dropzone - file upload negotiation
151151-- pico - styling
151151+- pico - styling, tabs, modals
152152- countable - dynamic input counter
153153154154## Contributing
+2-1
src/endpoints/account.tsx
···233233 // There has to be a better method for this tbh.
234234 const canMessageUser = await checkIfCanDMUser(c.env, bskyUserId);
235235 if (canMessageUser === false) {
236236- return c.json({ok: false, message: `Could not send a direct message to your bsky account.\nPlease check to see if you are following @${c.env.RESET_BOT_USERNAME} and your DM permissions`}, 401);
236236+ return c.json({ok: false, message:
237237+ `Could not send a direct message to your bsky account.\nPlease check to see if you are following @${c.env.RESET_BOT_USERNAME} and your DM permissions`}, 401);
237238 }
238239239240 const { data, error } = await auth.api.requestPasswordReset({
···11-import { PROGRESS_MADE, PROGRESS_TOTAL } from "../progress";
11+import { PROGRESS_MADE, PROGRESS_TOTAL } from "../../progress";
2233// Helper footer for various pages
44type FooterCopyrightProps = {
···991010export default function FooterCopyright(props: FooterCopyrightProps) {
1111 const newWinAttr = props.inNewWindow ? {"target": '_blank'} : {};
1212- const projectURL = (<a class="secondary" target="_blank" title="Project source on GitHub" href="https://github.com/SocksTheWolf/SkyScheduler">SkyScheduler</a>);
1212+ const projectURL = (<a class="secondary" target="_blank" title="Project source on GitHub"
1313+ href="https://github.com/SocksTheWolf/SkyScheduler">SkyScheduler</a>);
1314 const homepageURL = (<a class="secondary" title="Homepage" href="/">SkyScheduler</a>);
1415 const progressBarTooltip = `$${PROGRESS_MADE}/$${PROGRESS_TOTAL} for this month`;
1516 return (
···2122 <a rel="author" target="_blank" title="Project author" href="https://socksthewolf.com">SocksTheWolf</a><br />
2223 <small>
2324 <a class="secondary" target="_blank"
2424- data-tooltip="Tips are not required, the service is free, but if you like this service, tips would help with the costs <3"
2525+ data-tooltip="Tips are not required, the service is free, but if you like this service they are appreciated <3"
2526 title="Tip the dev" href="/tip">Tip</a> -
2627 <a class="secondary" {...newWinAttr} href="/tos" title="Terms of Service">Terms</a> -
2728 <a class="secondary" {...newWinAttr} href="/privacy" title="Privacy Policy">Privacy</a>
+3-3
src/layout/main.tsx
···11import { html } from 'hono/html';
22import { Child } from 'hono/jsx';
33-import { HtmlEscapedString } from 'hono/utils/html';
43import { PreloadRules } from '../types.d';
54import { mainScriptStr } from '../utils/appScripts';
66-import { PreloadDependencyTags } from './depTags';
77-import MetaTags from './metaTags';
55+import { PreloadDependencyTags } from './helpers/includesTags';
66+import MetaTags from './helpers/metaTags';
8798type BaseLayoutProps = {
109 children: Child;
···4544 </container>
4645 </body>
4746 </html>);
4747+ // inject the doctype so we're not in quirks mode
4848 return html`<!DOCTYPE html>
4949 ${layout}`;
5050}
+4-4
src/layout/makePost.tsx
···1111} from "../limits";
1212import { PreloadRules } from "../types.d";
1313import { ConstScriptPreload } from "../utils/constScriptGen";
1414-import { ContentLabelOptions } from "./contentLabelOptions";
1515-import { IncludeDependencyTags } from "./depTags";
1616-import { RetweetOptions } from "./retweetOptions";
1717-import { ScheduleOptions } from "./scheduleOptions";
1414+import { ContentLabelOptions } from "./options/contentLabelOptions";
1515+import { IncludeDependencyTags } from "./helpers/includesTags";
1616+import { RetweetOptions } from "./options/retweetOptions";
1717+import { ScheduleOptions } from "./options/scheduleOptions";
18181919export const PreloadPostCreation: PreloadRules[] = [
2020 ...ConstScriptPreload,
+5-3
src/layout/makeRetweet.tsx
···11-import { RetweetOptions } from "./retweetOptions";
22-import { ScheduleOptions } from "./scheduleOptions";
11+import { RetweetOptions } from "./options/retweetOptions";
22+import { ScheduleOptions } from "./options/scheduleOptions";
3344export function MakeRetweet() {
55 return (
···1111 <article>
1212 <header>Post To Retweet</header>
1313 <input type="text" id="repostRecordURL" placeholder="https://" />
1414- <small>This must be a post, it cannot be anything else. It must also exist, and be reachable (i.e. a post that's not deleted, nor are you forbidden from seeing it)</small>
1414+ <small>This must be a post, it cannot be anything else.
1515+ It must also exist, and be reachable
1616+ (i.e. a post that's not deleted, nor are you forbidden from seeing it)</small>
1517 </article>
1618 <ScheduleOptions timeID="repostTime" allowNow={false} type="retweet" header="Retweet At" />
1719 <article>
···11import { raw } from "hono/html";
2233export default function MetaTags() {
44+ /* Modify these to change meta information on all pages. */
45 const Title: string = "SkyScheduler";
56 const URL: string = "https://skyscheduler.work";
67 const Description: string = "Schedule and automatically repost on Bluesky! Boost engagement and reach more people no matter what time of day!";
···11import { Context } from "hono";
22-33-export function UseCFTurnstile(ctx: Context): boolean {
44- return ctx.env.SIGNUP_SETTINGS.use_captcha && ctx.env.IN_DEV === false;
55-}
22+import { UseCFTurnstile } from "../../utils/helpers";
6374export function TurnstileCaptchaPreloads(ctx: Context) {
85 if (UseCFTurnstile(ctx)) {
+3-2
src/layout/usernameField.tsx
···1010export function UsernameField(props?: UsernameFieldProps) {
1111 const hintText = props?.hintText ? raw(props.hintText) :
1212 (<span>This is your Bluesky username/handle, in the format of a custom domain or <code>USERNAME.bsky.social</code>.
1313- <br />Profile/post links will attempt to be converted to the correct format.
1313+ <br />Profile/post links will attempt to be converted into the correct format.
1414 </span>);
1515 // default required true.
1616 const inputRequired = (props) ? (props?.required || false) : true;
1717 return (
1818 <label>
1919 {props?.title || "Bluesky Handle"}
2020- <input type="text" id="username" name="username" autocomplete="username" minlength={BSKY_MIN_USERNAME_LENGTH} required={inputRequired} />
2020+ <input type="text" id="username" name="username" autocomplete="username"
2121+ minlength={BSKY_MIN_USERNAME_LENGTH} required={inputRequired} />
2122 <small>{hintText}</small>
2223 </label>
2324 );
-2
src/layout/violationsBar.tsx
···11import { Context } from "hono";
22-import isEmpty from "just-is-empty";
33-import { Violation } from "../types.d";
42import { getViolationsForCurrentUser } from "../utils/db/violations";
5364export async function ViolationNoticeBar(props: any) {
+1-1
src/middleware/turnstile.ts
···11import { Context } from "hono";
22-import { UseCFTurnstile } from "../layout/turnstile";
22+import { UseCFTurnstile } from "../utils/helpers";
3344// Middleware that handles turnstile verification.
55export async function verifyTurnstile(c: Context, next: any) {
+2-2
src/pages/dashboard.tsx
···11import { Context } from "hono";
22import { AltTextDialog } from "../layout/altTextModal";
33-import { IncludeDependencyTags, ScriptTags } from "../layout/depTags";
44-import FooterCopyright from "../layout/footer";
33+import { IncludeDependencyTags, ScriptTags } from "../layout/helpers/includesTags";
44+import FooterCopyright from "../layout/helpers/footer";
55import { BaseLayout } from "../layout/main";
66import { PostCreation, PreloadPostCreation } from "../layout/makePost";
77import { MakeRetweet } from "../layout/makeRetweet";
+3-3
src/pages/forgot.tsx
···11import { Context } from "hono";
22import AccountHandler from "../layout/account";
33-import FooterCopyright from "../layout/footer";
33+import FooterCopyright from "../layout/helpers/footer";
44import { BaseLayout } from "../layout/main";
55-import NavTags from "../layout/navTags";
66-import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/turnstile";
55+import NavTags from "../layout/helpers/navTags";
66+import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/helpers/turnstile";
77import { UsernameField } from "../layout/usernameField";
8899export default function ForgotPassword(props:any) {
+8-5
src/pages/homepage.tsx
···11-import FooterCopyright from "../layout/footer";
11+import FooterCopyright from "../layout/helpers/footer";
22import { BaseLayout } from "../layout/main";
33-import NavTags from "../layout/navTags";
33+import NavTags from "../layout/helpers/navTags";
44import {
55 MAX_POSTS_PER_THREAD, MAX_REPOST_DAYS, MAX_REPOST_IN_HOURS,
66 MAX_REPOST_INTERVAL, R2_FILE_SIZE_LIMIT_IN_MB
···1414 <article>
1515 <noscript><header>Javascript is required to use this website</header></noscript>
1616 <p>
1717- <strong>SkyScheduler</strong> is a free, <a href="https://github.com/socksthewolf/skyscheduler" rel="nofollow" target="_blank">open source</a> service that
1717+ <strong>SkyScheduler</strong> is a free,
1818+ <a href="https://github.com/socksthewolf/skyscheduler" rel="nofollow" target="_blank">open source</a> service that
1819 lets you schedule and automatically repost your content on Bluesky!<br />
1920 Boost engagement and reach more people no matter what time of day!<br />
2021 <center>
···3637 <li>Handles multiple users/accounts easily, supports most PDS instances</li>
3738 <li>Schedule your posts any time in the future (to the nearest hour)</li>
3839 <li>Supports embeds, quote posts, links, tagging, mentions</li>
3939- <li>Post <span data-tooltip={`images and video (up to ${R2_FILE_SIZE_LIMIT_IN_MB} MB)`}>media</span> with content labels and full support for alt text</li>
4040+ <li>Post <span data-tooltip={`images and video (up to ${R2_FILE_SIZE_LIMIT_IN_MB} MB)`}>media</span>
4141+ with content labels and full support for alt text</li>
4042 <li>Schedule entire threads with support of up to {MAX_POSTS_PER_THREAD} posts per thread!</li>
4141- <li>Automatically retweet your content at an interval of your choosing, up to {MAX_REPOST_INTERVAL} times every {MAX_REPOST_IN_HOURS-1} hours (or {MAX_REPOST_DAYS} days)</li>
4343+ <li>Automatically retweet your content at an interval of your choosing, up to {MAX_REPOST_INTERVAL} times every
4444+ {MAX_REPOST_IN_HOURS-1} hours (or {MAX_REPOST_DAYS} days)</li>
4245 <li>Edit the content of posts and alt text before they are posted</li>
4346 </ul>
4447 </p>
+1-1
src/pages/login.tsx
···11import AccountHandler from "../layout/account";
22import { BaseLayout } from "../layout/main";
33-import NavTags from "../layout/navTags";
33+import NavTags from "../layout/helpers/navTags";
44import { DashboardPasswordField } from "../layout/passwordFields";
55import { UsernameField } from "../layout/usernameField";
66import { PWAutoCompleteSettings } from "../types.d";
+2-2
src/pages/privacy.tsx
···11-import FooterCopyright from "../layout/footer";
11+import FooterCopyright from "../layout/helpers/footer";
22import { BaseLayout } from "../layout/main";
33-import NavTags from "../layout/navTags";
33+import NavTags from "../layout/helpers/navTags";
4455export default function PrivacyPolicy() {
66 return (
+1-1
src/pages/reset.tsx
···11import AccountHandler from "../layout/account";
22import { BaseLayout } from "../layout/main";
33-import NavTags from "../layout/navTags";
33+import NavTags from "../layout/helpers/navTags";
44import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits";
5566export default function ResetPassword() {
+3-3
src/pages/signup.tsx
···11import { Context } from "hono";
22import AccountHandler from "../layout/account";
33-import FooterCopyright from "../layout/footer";
33+import FooterCopyright from "../layout/helpers/footer";
44import { BaseLayout } from "../layout/main";
55-import NavTags from "../layout/navTags";
55+import NavTags from "../layout/helpers/navTags";
66import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/passwordFields";
77-import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/turnstile";
77+import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/helpers/turnstile";
88import { UsernameField } from "../layout/usernameField";
99import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits";
1010import { PWAutoCompleteSettings } from "../types.d";
+2-2
src/pages/tos.tsx
···11-import FooterCopyright from "../layout/footer";
11+import FooterCopyright from "../layout/helpers/footer";
22import { BaseLayout } from "../layout/main";
33-import NavTags from "../layout/navTags";
33+import NavTags from "../layout/helpers/navTags";
4455export default function TermsOfService() {
66 return (
-1
src/utils/db/maintain.ts
···22import { BatchItem } from "drizzle-orm/batch";
33import { drizzle, DrizzleD1Database } from "drizzle-orm/d1";
44import flatten from "just-flatten-it";
55-import truncate from "just-truncate";
65import { mediaFiles, posts, repostCounts, reposts } from "../../db/app.schema";
76import { users } from "../../db/auth.schema";
87import { MAX_POSTED_LENGTH } from "../../limits";