Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers

layout updates

+197 -202
+9
src/layout/buttons/logout.tsx
··· 1 + 2 + export default function LogoutButton() { 3 + return (<div> 4 + <button class="outline w-full btn-error logout" hx-post="/account/logout" 5 + hx-target="body" hx-confirm="Are you sure you want to logout?"> 6 + Logout 7 + </button> 8 + </div>); 9 + };
+12
src/layout/buttons/refresh.tsx
··· 1 + 2 + export default function RefreshPostsButton() { 3 + return (<> 4 + <button id="refresh-posts" hx-get="/post/all" hx-target="#posts" 5 + hx-trigger="refreshPosts from:body, accountUpdated from:body, click throttle:3s" 6 + hx-on-htmx-before-request="this.classList.add('svgAnim');" 7 + hx-on-htmx-after-request="setTimeout(() => {this.classList.remove('svgAnim')}, 3000)"> 8 + <span>Refresh Posts</span> 9 + <img src="/icons/refresh.svg" height="20px" width="20px" alt="refresh icon" /> 10 + </button> 11 + </>); 12 + };
+7
src/layout/buttons/settings.tsx
··· 1 + 2 + export default function SettingsButton() { 3 + return (<button class="outline contrast" id="settingsButton"> 4 + <span>Account Settings</span> 5 + <img src="/icons/settings.svg" height="20px" width="20px" alt="settings gear" /> 6 + </button>); 7 + }
+38
src/layout/fields/passwordFields.tsx
··· 1 + import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../../limits"; 2 + import { PWAutoCompleteSettings } from "../../types"; 3 + 4 + type PasswordFieldSettings = { 5 + required?: boolean 6 + } 7 + 8 + type DashboardPasswordFieldSettings = { 9 + required?: boolean 10 + autocomplete: PWAutoCompleteSettings 11 + } 12 + 13 + export function BSkyAppPasswordField(props: PasswordFieldSettings) { 14 + return (<input type="password" name="bskyAppPassword" title="Bluesky account's App Password" 15 + maxlength={BSKY_MAX_APP_PASSWORD_LENGTH} placeholder="" required={props.required || undefined} 16 + data-1p-ignore data-bwignore data-lpignore="true" 17 + data-protonpass-ignore="true" 18 + autocomplete="off"></input>); 19 + } 20 + 21 + export function DashboardPasswordField(props: DashboardPasswordFieldSettings) { 22 + let autocompleteSetting: string = ""; 23 + switch (props.autocomplete) { 24 + default: 25 + case PWAutoCompleteSettings.Off: 26 + autocompleteSetting = "off"; 27 + break; 28 + case PWAutoCompleteSettings.CurrentPass: 29 + autocompleteSetting = "current-password"; 30 + break; 31 + case PWAutoCompleteSettings.NewPass: 32 + autocompleteSetting = "new-password"; 33 + break; 34 + } 35 + return (<input id="password" type="password" name="password" minlength={MIN_DASHBOARD_PASS} 36 + maxlength={MAX_DASHBOARD_PASS} required={props.required || undefined} 37 + autocomplete={autocompleteSetting} />); 38 + }
+23
src/layout/fields/usernameField.tsx
··· 1 + import { raw } from "hono/html"; 2 + import { BSKY_MIN_USERNAME_LENGTH } from "../../limits"; 3 + 4 + type UsernameFieldProps = { 5 + title?: string; 6 + hintText?: string; 7 + required?: boolean; 8 + }; 9 + 10 + export default function UsernameField(props?: UsernameFieldProps) { 11 + const hintText = props?.hintText ? raw(props.hintText) : 12 + (<span>This is your Bluesky username/handle, in the format of a custom domain or <code>USERNAME.bsky.social</code>. 13 + <br />Profile/post links will attempt to be converted into the correct format. 14 + </span>); 15 + // default required true. 16 + const inputRequired = (props) ? (props?.required || false) : true; 17 + return (<label> 18 + {props?.title || "Bluesky Handle"} 19 + <input type="text" id="username" name="username" autocomplete="username" 20 + minlength={BSKY_MIN_USERNAME_LENGTH} required={inputRequired} /> 21 + <small>{hintText}</small> 22 + </label>); 23 + };
+5 -7
src/layout/makePost.tsx
··· 9 9 import { APP_NAME } from "../siteinfo"; 10 10 import { ConstScriptStr } from "../utils/constScriptGen"; 11 11 import { IncludeDependencyTags, PreloadRules } from "./helpers/includesTags"; 12 - import { ContentLabelOptions } from "./options/contentLabelOptions"; 13 - import { RetweetOptions } from "./options/retweetOptions"; 14 - import { ScheduleOptions } from "./options/scheduleOptions"; 12 + import ContentLabelOptions from "./options/contentLabelOptions"; 13 + import RetweetOptions from "./options/retweetOptions"; 14 + import ScheduleOptions from "./options/scheduleOptions"; 15 15 16 16 export const PreloadPostCreation: PreloadRules[] = [ 17 17 {type: "script", href: ConstScriptStr }, ··· 25 25 export function PostCreation({ctx}: any) { 26 26 const maxWidth: number|undefined = ctx.env.IMAGE_SETTINGS.max_width; 27 27 const bskyImageLimits = `Max file size of ${BSKY_IMG_SIZE_LIMIT_IN_MB}MB`; 28 - return ( 29 - <section> 28 + return (<section> 30 29 <IncludeDependencyTags scripts={PreloadPostCreation} /> 31 30 <article> 32 31 <form id="postForm" novalidate> ··· 120 119 </footer> 121 120 </form> 122 121 </article> 123 - </section> 124 - ); 122 + </section>); 125 123 }
+2 -2
src/layout/makeRetweet.tsx
··· 1 1 import { MAX_POSTED_LENGTH } from "../limits"; 2 - import { RetweetOptions } from "./options/retweetOptions"; 3 - import { ScheduleOptions } from "./options/scheduleOptions"; 2 + import RetweetOptions from "./options/retweetOptions"; 3 + import ScheduleOptions from "./options/scheduleOptions"; 4 4 5 5 export function MakeRetweet() { 6 6 return (<article>
+18 -20
src/layout/options/contentLabelOptions.tsx
··· 1 1 2 2 type ContentLabelProps = { 3 3 id: string; 4 - } 5 - export function ContentLabelOptions(props: ContentLabelProps) 6 - { 7 - return ( 8 - <article> 9 - <header>Content Label</header> 10 - <select name="label" id={props.id}> 11 - <option disabled selected value=""> -- select an option -- </option> 12 - <option value="None">Safe</option> 13 - <option value="Suggestive">Suggestive</option> 14 - <option value="Nudity">Nudity (non-sexual nudity)</option> 15 - <option value="Adult">Adult (porn)</option> 16 - <option disabled value="">---</option> 17 - <option value="Graphic">Graphic Media (gore/violence)</option> 18 - <option value="GraphicAdult">Adult Graphic Media (gore/violence)</option> 19 - </select> 20 - <small>Remember to set the appropriate content label for your content</small> 21 - </article> 22 - ) 23 - } 4 + }; 5 + 6 + export default function ContentLabelOptions(props: ContentLabelProps) { 7 + return (<article> 8 + <header>Content Label</header> 9 + <select name="label" id={props.id}> 10 + <option disabled selected value=""> -- select an option -- </option> 11 + <option value="None">Safe</option> 12 + <option value="Suggestive">Suggestive</option> 13 + <option value="Nudity">Nudity (non-sexual nudity)</option> 14 + <option value="Adult">Adult (porn)</option> 15 + <option disabled value="">---</option> 16 + <option value="Graphic">Graphic Media (gore/violence)</option> 17 + <option value="GraphicAdult">Adult Graphic Media (gore/violence)</option> 18 + </select> 19 + <small>Remember to set the appropriate content label for your content</small> 20 + </article>); 21 + };
+1 -1
src/layout/options/retweetOptions.tsx
··· 10 10 checkboxLabel?: string; 11 11 }; 12 12 13 - export function RetweetOptions(props: RetweetOptionsProps) { 13 + export default function RetweetOptions(props: RetweetOptionsProps) { 14 14 const repostedFrom = !isEmpty(props.timeString) ? props.timeString : "the post time"; 15 15 const checkboxLabel = !isEmpty(props.checkboxLabel) ? props.checkboxLabel : "Should Auto-Retweet?"; 16 16 return (
+15 -17
src/layout/options/scheduleOptions.tsx
··· 8 8 header?: string; 9 9 }; 10 10 11 - export function ScheduleOptions(props: ScheduleOptionsProps) { 11 + export default function ScheduleOptions(props: ScheduleOptionsProps) { 12 12 const hasHeader = !isEmpty(props.header); 13 13 const headerText = hasHeader ? props.header : ""; 14 14 ··· 18 18 <label class="noselect capitialize checkboxLine" for={props.checkboxID}>Make {props.type} Now?</label> 19 19 </div>) : null; 20 20 21 - return ( 22 - <section class="scheduledDateBlock"> 23 - <article> 24 - <header hidden={!hasHeader}>{headerText}</header> 25 - <input class="timeSelector" type="datetime-local" id={props.timeID} placeholder="" required /> 26 - <small>Time is based on your device's current timezone and is automatically converted for you.</small> 27 - {postNowHTML} 28 - <footer> 29 - <small> 30 - <i>You can schedule {props.type}s in the future, hourly. Time is rounded down to the nearest hour.</i> 31 - </small> 32 - </footer> 33 - </article> 34 - </section> 35 - ); 36 - } 21 + return (<section class="scheduledDateBlock"> 22 + <article> 23 + <header hidden={!hasHeader}>{headerText}</header> 24 + <input class="timeSelector" type="datetime-local" id={props.timeID} placeholder="" required /> 25 + <small>Time is based on your device's current timezone and is automatically converted for you.</small> 26 + {postNowHTML} 27 + <footer> 28 + <small> 29 + <i>You can schedule {props.type}s in the future, hourly. Time is rounded down to the nearest hour.</i> 30 + </small> 31 + </footer> 32 + </article> 33 + </section>); 34 + };
-40
src/layout/passwordFields.tsx
··· 1 - import { html } from "hono/html"; 2 - import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits"; 3 - import { PWAutoCompleteSettings } from "../types"; 4 - 5 - type PasswordFieldSettings = { 6 - required?: boolean 7 - } 8 - 9 - type DashboardPasswordFieldSettings = { 10 - required?: boolean 11 - autocomplete: PWAutoCompleteSettings 12 - } 13 - 14 - export function BSkyAppPasswordField(props: PasswordFieldSettings) { 15 - const requiredAttr:string = props.required ? "required" : ""; 16 - return html`<input type="password" name="bskyAppPassword" title="Bluesky account's App Password" 17 - maxlength=${BSKY_MAX_APP_PASSWORD_LENGTH} placeholder="" ${requiredAttr} 18 - data-1p-ignore data-bwignore data-lpignore="true" 19 - data-protonpass-ignore="true" 20 - autocomplete="off"></input>`; 21 - } 22 - 23 - export function DashboardPasswordField(props: DashboardPasswordFieldSettings) { 24 - const requiredAttr:string = props.required ? "required" : ""; 25 - let autocompleteSetting:string = ""; 26 - switch (props.autocomplete) { 27 - default: 28 - case PWAutoCompleteSettings.Off: 29 - autocompleteSetting = "off"; 30 - break; 31 - case PWAutoCompleteSettings.CurrentPass: 32 - autocompleteSetting = "current-password"; 33 - break; 34 - case PWAutoCompleteSettings.NewPass: 35 - autocompleteSetting = "new-password"; 36 - break; 37 - } 38 - return html`<input id="password" type="password" name="password" minlength=${MIN_DASHBOARD_PASS} 39 - maxlength=${MAX_DASHBOARD_PASS} ${requiredAttr} autocomplete=${autocompleteSetting} />`; 40 - }
src/layout/posts/buttons.tsx src/layout/buttons/posts.tsx
+4 -10
src/layout/settings.tsx
··· 2 2 import { APP_NAME } from "../siteinfo"; 3 3 import { PWAutoCompleteSettings } from "../types"; 4 4 import { settingsScriptStr } from "../utils/appScripts"; 5 - import { BSkyAppPasswordField, DashboardPasswordField } from "./passwordFields"; 6 - import { UsernameField } from "./usernameField"; 5 + import { BSkyAppPasswordField, DashboardPasswordField } from "./fields/passwordFields"; 6 + import UsernameField from "./fields/usernameField"; 7 7 8 8 type SettingsTypeProps = { 9 9 pds?: string; ··· 24 24 <br /> 25 25 <section> 26 26 <form id="settingsData" name="settingsData" hx-post="/account/update" hx-target="#accountResponse" 27 - hx-swap="innerHTML swap:1s" hx-indicator="#spinner" hx-disabled-elt="#settingsButtons button, find input" novalidate> 27 + hx-swap="innerHTML swap:1s" hx-indicator="#spinner" 28 + hx-disabled-elt="#settingsButtons button, find input" novalidate> 28 29 29 30 <UsernameField required={false} title="BlueSky Handle:" 30 31 hintText="Only change this if you have recently changed your Bluesky handle" /> ··· 86 87 <script type="text/javascript" src={settingsScriptStr}></script> 87 88 </>); 88 89 } 89 - 90 - export function SettingsButton() { 91 - return (<button class="outline contrast" id="settingsButton"> 92 - <span>Account Settings</span> 93 - <img src="/icons/settings.svg" height="20px" width="20px" alt="settings gear" /> 94 - </button>); 95 - }
-25
src/layout/usernameField.tsx
··· 1 - import { raw } from "hono/html"; 2 - import { BSKY_MIN_USERNAME_LENGTH } from "../limits"; 3 - 4 - type UsernameFieldProps = { 5 - title?: string; 6 - hintText?: string; 7 - required?: boolean; 8 - }; 9 - 10 - export function UsernameField(props?: UsernameFieldProps) { 11 - const hintText = props?.hintText ? raw(props.hintText) : 12 - (<span>This is your Bluesky username/handle, in the format of a custom domain or <code>USERNAME.bsky.social</code>. 13 - <br />Profile/post links will attempt to be converted into the correct format. 14 - </span>); 15 - // default required true. 16 - const inputRequired = (props) ? (props?.required || false) : true; 17 - return ( 18 - <label> 19 - {props?.title || "Bluesky Handle"} 20 - <input type="text" id="username" name="username" autocomplete="username" 21 - minlength={BSKY_MIN_USERNAME_LENGTH} required={inputRequired} /> 22 - <small>{hintText}</small> 23 - </label> 24 - ); 25 - }
+6 -14
src/pages/dashboard.tsx
··· 1 1 import { Context } from "hono"; 2 2 import { AltTextDialog } from "../layout/altTextModal"; 3 + import LogoutButton from "../layout/buttons/logout"; 4 + import RefreshPostsButton from "../layout/buttons/refresh"; 5 + import SettingsButton from "../layout/buttons/settings"; 3 6 import FooterCopyright from "../layout/helpers/footer"; 4 7 import { IncludeDependencyTags, PreloadRules } from "../layout/helpers/includesTags"; 5 8 import { LogoImage } from "../layout/helpers/logo"; ··· 7 10 import { PostCreation, PreloadPostCreation } from "../layout/makePost"; 8 11 import { MakeRetweet } from "../layout/makeRetweet"; 9 12 import { ScheduledPostList } from "../layout/postList"; 10 - import { Settings, SettingsButton } from "../layout/settings"; 13 + import { Settings } from "../layout/settings"; 11 14 import { ViolationNoticeBar } from "../layout/violationsBar"; 12 15 import { APP_NAME, DASHBOARD_TAG_LINE, SHOW_SUPPORT_PROGRESS_BAR } from "../siteinfo"; 13 16 import { ··· 47 50 hx-trigger="accountUpdated from:body, load once" hx-target="this"></b></small> 48 51 </div> 49 52 <center class="postControls"> 50 - <button id="refresh-posts" hx-get="/post/all" hx-target="#posts" 51 - hx-trigger="refreshPosts from:body, accountUpdated from:body, click throttle:3s" 52 - hx-on-htmx-before-request="this.classList.add('svgAnim');" 53 - hx-on-htmx-after-request="setTimeout(() => {this.classList.remove('svgAnim')}, 3000)"> 54 - <span>Refresh Posts</span> 55 - <img src="/icons/refresh.svg" height="20px" width="20px" alt="refresh icon" /> 56 - </button> 53 + <RefreshPostsButton /> 57 54 <SettingsButton /> 58 55 </center> 59 56 <hr /> ··· 63 60 <ScheduledPostList ctx={ctx} /> 64 61 </div> 65 62 <footer> 66 - <div> 67 - <button class="outline w-full btn-error logout" hx-post="/account/logout" 68 - hx-target="body" hx-confirm="Are you sure you want to logout?"> 69 - Logout 70 - </button> 71 - </div> 63 + <LogoutButton /> 72 64 <hr /> 73 65 <FooterCopyright inNewWindow={true} showHomepage={true} showProgressBar={SHOW_SUPPORT_PROGRESS_BAR} showVersion={true} /> 74 66 </footer>
+4 -8
src/pages/forgot.tsx
··· 1 1 import { Context } from "hono"; 2 2 import AccountHandler from "../layout/account"; 3 + import UsernameField from "../layout/fields/usernameField"; 3 4 import FooterCopyright from "../layout/helpers/footer"; 4 5 import NavTags from "../layout/helpers/navTags"; 5 6 import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/helpers/turnstile"; 6 7 import { BaseLayout } from "../layout/main"; 7 - import { UsernameField } from "../layout/usernameField"; 8 8 import { APP_NAME } from "../siteinfo"; 9 9 10 10 export default function ForgotPassword(props:any) { 11 11 const ctx: Context = props.c; 12 12 const botAccountURL: string = `https://bsky.app/profile/${ctx.env.RESET_BOT_USERNAME}`; 13 - return ( 14 - <BaseLayout title="Forgot Password" 15 - preloads={[...TurnstileCaptchaPreloads(ctx)]} noIndex={true}> 13 + return (<BaseLayout title="Forgot Password" 14 + preloads={[...TurnstileCaptchaPreloads(ctx)]} noIndex={true}> 16 15 <NavTags /> 17 16 <AccountHandler title="Forgot Password Reset" 18 17 submitText={`Request ${APP_NAME} Password Reset`} ··· 29 28 It is <u>heavily recommended</u> to <a href={botAccountURL} target="_blank">follow the service account</a>.<br /><br /> 30 29 <small><b>NOTE</b>: {APP_NAME} sends DMs via an one-way delivery method. No one (other than you) can see the account password reset URL.</small></p> 31 30 </center> 32 - 33 31 <UsernameField /> 34 - 35 32 <TurnstileCaptcha c={ctx} /> 36 33 </AccountHandler> 37 - </BaseLayout> 38 - ); 34 + </BaseLayout>); 39 35 }
+18 -21
src/pages/login.tsx
··· 1 1 import AccountHandler from "../layout/account"; 2 2 import NavTags from "../layout/helpers/navTags"; 3 3 import { BaseLayout } from "../layout/main"; 4 - import { DashboardPasswordField } from "../layout/passwordFields"; 5 - import { UsernameField } from "../layout/usernameField"; 4 + import { DashboardPasswordField } from "../layout/fields/passwordFields"; 5 + import UsernameField from "../layout/fields/usernameField"; 6 6 import { APP_NAME } from "../siteinfo"; 7 7 import { PWAutoCompleteSettings } from "../types"; 8 8 9 9 export default function Login() { 10 10 const links = [{title: "Sign Up", url: "/signup"}, {title: "Forgot Password", url: "/forgot"}]; 11 - return ( 12 - <BaseLayout title="Login"> 13 - <NavTags /> 14 - <AccountHandler title="Login" 15 - loadingText="Logging in..." 16 - footerLinks={links} 17 - endpoint="/account/login" 18 - successText="Success! Redirecting to dashboard..." 19 - redirect="/dashboard"> 11 + return (<BaseLayout title="Login"> 12 + <NavTags /> 13 + <AccountHandler title="Login" 14 + loadingText="Logging in..." 15 + footerLinks={links} 16 + endpoint="/account/login" 17 + successText="Success! Redirecting to dashboard..." 18 + redirect="/dashboard"> 20 19 21 - <UsernameField /> 22 - 23 - <label hx-history="false"> 24 - {APP_NAME} Dashboard Password 25 - <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} /> 26 - <small><b>NOTE</b>: This password is not related to your Bluesky account!</small> 27 - </label> 28 - </AccountHandler> 29 - </BaseLayout> 30 - ); 20 + <UsernameField /> 21 + <label hx-history="false"> 22 + {APP_NAME} Dashboard Password 23 + <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} /> 24 + <small><b>NOTE</b>: This password is not related to your Bluesky account!</small> 25 + </label> 26 + </AccountHandler> 27 + </BaseLayout>); 31 28 }
+35 -37
src/pages/signup.tsx
··· 4 4 import NavTags from "../layout/helpers/navTags"; 5 5 import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/helpers/turnstile"; 6 6 import { BaseLayout } from "../layout/main"; 7 - import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/passwordFields"; 8 - import { UsernameField } from "../layout/usernameField"; 7 + import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/fields/passwordFields"; 8 + import UsernameField from "../layout/fields/usernameField"; 9 9 import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits"; 10 10 import { APP_NAME } from "../siteinfo"; 11 11 import { PWAutoCompleteSettings } from "../types"; ··· 17 17 (<a href={getInviteThread(ctx)} target="_blank">Invite codes are routinely posted in this thread, grab one here</a>) : 18 18 "You can ask for the maintainer for it"; 19 19 20 - return ( 21 - <BaseLayout title="Signup" 22 - preloads={[...TurnstileCaptchaPreloads(ctx)]}> 20 + return (<BaseLayout title="Signup" 21 + preloads={[...TurnstileCaptchaPreloads(ctx)]}> 23 22 <NavTags /> 24 23 <AccountHandler title="Create Account" 25 24 submitText="Sign Up!" ··· 29 28 redirect="/login" 30 29 footerHTML={<FooterCopyright />}> 31 30 32 - <UsernameField /> 31 + <UsernameField /> 33 32 34 - <label hx-history="false"> 35 - {APP_NAME} Dashboard Password 36 - <DashboardPasswordField autocomplete={PWAutoCompleteSettings.NewPass} required={true} /> 37 - <small>Create a new password to use to login to {APP_NAME}. Passwords should be {MIN_DASHBOARD_PASS} to {MAX_DASHBOARD_PASS} characters long.</small> 38 - </label> 33 + <label hx-history="false"> 34 + {APP_NAME} Dashboard Password 35 + <DashboardPasswordField autocomplete={PWAutoCompleteSettings.NewPass} required={true} /> 36 + <small>Create a new password to use to login to {APP_NAME}. Passwords should be {MIN_DASHBOARD_PASS} to {MAX_DASHBOARD_PASS} characters long.</small> 37 + </label> 39 38 39 + <label> 40 + Bluesky App Password 41 + <BSkyAppPasswordField required={true} /> 42 + <small> 43 + If you need a BlueSky app password for your account, <a target="_blank" href="https://bsky.app/settings/app-passwords">you can get one here</a>.<br /> 44 + If you use a separate PDS, you can change that in "Account Settings" on your dashboard, the site will attempt to infer your PDS for you. 45 + </small> 46 + </label> 47 + 48 + {isUsingInviteKeys(ctx) ? ( 40 49 <label> 41 - Bluesky App Password 42 - <BSkyAppPasswordField required={true} /> 43 - <small> 44 - If you need a BlueSky app password for your account, <a target="_blank" href="https://bsky.app/settings/app-passwords">you can get one here</a>.<br /> 45 - If you use a separate PDS, you can change that in "Account Settings" on your dashboard, the site will attempt to infer your PDS for you. 46 - </small> 50 + {APP_NAME} Invite Key/Signup Token 51 + <input type="text" name="signupToken" placeholder="" required /> 52 + <small>This is an invite key to try to dissuade bots/automated applications. {linkToInvites}.</small> 47 53 </label> 48 - 49 - {isUsingInviteKeys(ctx) ? ( 50 - <label> 51 - {APP_NAME} Invite Key/Signup Token 52 - <input type="text" name="signupToken" placeholder="" required /> 53 - <small>This is an invite key to try to dissuade bots/automated applications. {linkToInvites}.</small> 54 - </label> 55 - ) : ''} 54 + ) : ''} 56 55 57 - <hr /> 58 - <fieldset> 59 - <legend><label for="agreeTerms">Agree to {APP_NAME} Terms</label></legend> 60 - <input id="agreeTerms" type="checkbox" name="agreeTerms" /> 61 - Check the box if you agree to {APP_NAME}'s <a href="/privacy" class="secondary" target="_blank" title="link to privacy policy">privacy policy 62 - </a> and <a href="/tos" class="secondary" target="_blank" title="link to terms of service">terms of service</a>. 63 - </fieldset> 64 - <br /> 65 - <TurnstileCaptcha c={ctx} /> 66 - </AccountHandler> 67 - </BaseLayout> 68 - ); 56 + <hr /> 57 + <fieldset> 58 + <legend><label for="agreeTerms">Agree to {APP_NAME} Terms</label></legend> 59 + <input id="agreeTerms" type="checkbox" name="agreeTerms" /> 60 + Check the box if you agree to {APP_NAME}'s <a href="/privacy" class="secondary" target="_blank" title="link to privacy policy">privacy policy 61 + </a> and <a href="/tos" class="secondary" target="_blank" title="link to terms of service">terms of service</a>. 62 + </fieldset> 63 + <br /> 64 + <TurnstileCaptcha c={ctx} /> 65 + </AccountHandler> 66 + </BaseLayout>); 69 67 }