pancy fants
This commit is contained in:
parent
a4fd3a3556
commit
b6633d6f25
15 changed files with 339 additions and 116 deletions
|
@ -18,8 +18,8 @@ export default function FancyTextEditor({
|
||||||
value: _value,
|
value: _value,
|
||||||
onInput,
|
onInput,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
className,
|
className: extraClasses = '',
|
||||||
placeholder,
|
placeholder = '',
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
const divRef = useRef<HTMLDivElement>(null)
|
const divRef = useRef<HTMLDivElement>(null)
|
||||||
const [hasFocus, setHasFocus] = useState(false)
|
const [hasFocus, setHasFocus] = useState(false)
|
||||||
|
@ -39,7 +39,7 @@ export default function FancyTextEditor({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value && !hasFocus) {
|
if (!value && !hasFocus) {
|
||||||
div.innerText = placeholder ?? ''
|
div.innerText = placeholder
|
||||||
} else if (div.innerText !== value) {
|
} else if (div.innerText !== value) {
|
||||||
div.innerText = value
|
div.innerText = value
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default function FancyTextEditor({
|
||||||
const blurListener = () => {
|
const blurListener = () => {
|
||||||
setHasFocus(false)
|
setHasFocus(false)
|
||||||
if (!value) {
|
if (!value) {
|
||||||
div.innerText = placeholder ?? ''
|
div.innerText = placeholder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export default function FancyTextEditor({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={divRef}
|
ref={divRef}
|
||||||
className={`w-full p-3 resize-none border border-gray-200 rounded-md focus:outline-none focus:border-gray-300 ${textColour} min-h-30 ${className ?? ''}`}
|
className={`text-input w-full min-h-30 ${textColour} ${extraClasses}`}
|
||||||
contentEditable
|
contentEditable
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
|
|
11
src/components/NavBar.tsx
Normal file
11
src/components/NavBar.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
|
export default function NavBar() {
|
||||||
|
return (
|
||||||
|
<nav className={`w-full flex flex-row-reverse px-4 md:px-8 py-0.5`}>
|
||||||
|
<Link className={`text-gray-800`} to="/signup">
|
||||||
|
create account
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
import { useState, ChangeEvent } from 'react'
|
import { useState } from 'react'
|
||||||
import FancyTextEditor, { TextInputKeyDownEvent } from './FancyTextEditor.tsx'
|
import FancyTextEditor, { TextInputKeyDownEvent } from './FancyTextEditor.tsx'
|
||||||
|
import PrimaryButton from './PrimaryButton.tsx'
|
||||||
|
import SecondaryButton from './SecondaryButton.tsx'
|
||||||
|
import { openFileDialog } from '../utils/openFileDialog.ts'
|
||||||
|
|
||||||
interface NewPostWidgetProps {
|
interface NewPostWidgetProps {
|
||||||
onSubmit: (content: string, media: File[]) => void
|
onSubmit: (content: string, media: File[]) => void
|
||||||
|
@ -20,20 +23,19 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
|
||||||
setContent(value)
|
setContent(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMediaChange = (e: ChangeEvent<HTMLInputElement>) => {
|
async function onAddMediaClicked() {
|
||||||
const inputEl = e.target as HTMLInputElement
|
const files = await openFileDialog('image/*', true)
|
||||||
if (inputEl.files == null || inputEl.files.length === 0) {
|
if (files == null || files.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFiles = Array.from(inputEl.files).map((file) => ({
|
const newFiles = Array.from(files).map((file) => ({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
file,
|
file,
|
||||||
objectUrl: URL.createObjectURL(file),
|
objectUrl: URL.createObjectURL(file),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setAttachments((attachments) => [...attachments, ...newFiles])
|
setAttachments((attachments) => [...attachments, ...newFiles])
|
||||||
inputEl.value = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
@ -90,24 +92,13 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<label className="cursor-pointer text-gray-500 hover:text-gray-700">
|
<SecondaryButton onClick={onAddMediaClicked}>+ add media</SecondaryButton>
|
||||||
<input
|
<PrimaryButton
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleMediaChange}
|
|
||||||
className="hidden"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<span className="flex items-center">+ Add media</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 bg-gray-800 text-white rounded-md hover:bg-gray-700 disabled:opacity-50"
|
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)}
|
disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)}
|
||||||
>
|
>
|
||||||
Post
|
post
|
||||||
</button>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
27
src/components/PrimaryButton.tsx
Normal file
27
src/components/PrimaryButton.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
interface PrimaryButtonProps {
|
||||||
|
disabled?: boolean
|
||||||
|
type?: 'submit' | 'button'
|
||||||
|
onClick?: () => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PrimaryButton({
|
||||||
|
disabled = false,
|
||||||
|
type = 'button',
|
||||||
|
onClick = () => {},
|
||||||
|
className: extraClasses = '',
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<PrimaryButtonProps>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`primary-button ${extraClasses}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
18
src/components/PrimaryLinkButton.tsx
Normal file
18
src/components/PrimaryLinkButton.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
|
||||||
|
interface PrimaryLinkButtonProps {
|
||||||
|
href: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PrimaryLinkButton({
|
||||||
|
href,
|
||||||
|
className: extraClasses = '',
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<PrimaryLinkButtonProps>) {
|
||||||
|
return (
|
||||||
|
<a href={href} className={`primary-button text-center ${extraClasses}`}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
31
src/components/SecondaryButton.tsx
Normal file
31
src/components/SecondaryButton.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { PropsWithChildren } from 'react'
|
||||||
|
interface PrimaryButtonProps {
|
||||||
|
disabled?: boolean
|
||||||
|
type?: 'submit' | 'button'
|
||||||
|
onClick?: () => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SecondaryButton({
|
||||||
|
disabled = false,
|
||||||
|
type = 'button',
|
||||||
|
onClick = () => {},
|
||||||
|
className: extraClasses = '',
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<PrimaryButtonProps>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`
|
||||||
|
px-4 p-2 rounded-md
|
||||||
|
text-primary-500 hover:text-primary-700
|
||||||
|
cursor-pointer disabled:cursor-default
|
||||||
|
${extraClasses}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
31
src/components/TextInput.tsx
Normal file
31
src/components/TextInput.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
interface TextInputProps {
|
||||||
|
id?: string
|
||||||
|
type?: 'text' | 'email' | 'password'
|
||||||
|
value: string
|
||||||
|
onInput: (value: string) => void
|
||||||
|
className?: string
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TextInput({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
onInput,
|
||||||
|
className: extraClasses = '',
|
||||||
|
placeholder = '',
|
||||||
|
type = 'text',
|
||||||
|
required = false,
|
||||||
|
}: TextInputProps) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
type={type}
|
||||||
|
required={required}
|
||||||
|
onChange={(e) => onInput(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={`text-input w-full ${extraClasses}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -28,5 +28,5 @@ export function useFeedViewModel(
|
||||||
}
|
}
|
||||||
}, [loadMore, hasMore])
|
}, [loadMore, hasMore])
|
||||||
|
|
||||||
return [pages, setPages, loadNextPage] as const
|
return { pages, setPages, loadNextPage } as const
|
||||||
}
|
}
|
|
@ -6,4 +6,66 @@
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer variables {
|
||||||
|
:root {
|
||||||
|
/* TODO find out if tailwind exposes these spacing variables already */
|
||||||
|
--spacing-2: calc(var(--spacing) * 2);
|
||||||
|
--spacing-4: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary-50: var(--color-gray-50);
|
||||||
|
--color-primary-100: var(--color-gray-100);
|
||||||
|
--color-primary-200: var(--color-gray-200);
|
||||||
|
--color-primary-300: var(--color-gray-300);
|
||||||
|
--color-primary-400: var(--color-gray-400);
|
||||||
|
--color-primary-500: var(--color-gray-500);
|
||||||
|
--color-primary-600: var(--color-gray-600);
|
||||||
|
--color-primary-700: var(--color-gray-700);
|
||||||
|
--color-primary-800: var(--color-gray-800);
|
||||||
|
--color-primary-900: var(--color-gray-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/*
|
||||||
|
component class for text-inputs
|
||||||
|
as of writing this we want to reuse this for FancyTextEditor and TextInput
|
||||||
|
which is shitty and difficult to do by creating some react component with tailwind
|
||||||
|
it might be they need to diverge in the future, in which case i suppose we can
|
||||||
|
split this up, and potentially inline the tailwind classes
|
||||||
|
*/
|
||||||
|
.text-input {
|
||||||
|
padding: var(--spacing-2);
|
||||||
|
resize: none;
|
||||||
|
border: 1px solid var(--color-gray-200);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button {
|
||||||
|
padding: var(--spacing-2) var(--spacing-4);
|
||||||
|
background: var(--color-primary-800);
|
||||||
|
color: var(--color-white);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:hover {
|
||||||
|
background: var(--color-primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-button:disabled {
|
||||||
|
opacity: 50%;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
import { PropsWithChildren } from 'react'
|
|
||||||
import { Link } from 'react-router'
|
|
||||||
|
|
||||||
export default function AppLayout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<nav className={`w-full flex flex-row-reverse px-4 md:px-8 py-0.5`}>
|
|
||||||
<Link className={`text-gray-800`} to="/signup">
|
|
||||||
create account
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
<main className={`w-full`}>{children}</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
17
src/layouts/SingleColumnLayout.tsx
Normal file
17
src/layouts/SingleColumnLayout.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { PropsWithChildren, ReactNode } from 'react'
|
||||||
|
|
||||||
|
interface SingleColumnLayoutProps {
|
||||||
|
navbar?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SingleColumnLayout({
|
||||||
|
children,
|
||||||
|
navbar,
|
||||||
|
}: PropsWithChildren<SingleColumnLayoutProps>) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{navbar}
|
||||||
|
<main className={`w-full max-w-3xl mx-auto`}>{children}</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,6 +2,9 @@ import { useCallback } from 'react'
|
||||||
import FeedView from '../feed/FeedView.tsx'
|
import FeedView from '../feed/FeedView.tsx'
|
||||||
import { PostsService } from '../model/posts/postsService.ts'
|
import { PostsService } from '../model/posts/postsService.ts'
|
||||||
import { useParams } from 'react-router'
|
import { useParams } from 'react-router'
|
||||||
|
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
|
||||||
|
import NavBar from '../components/NavBar.tsx'
|
||||||
|
import { useFeedViewModel } from '../feed/FeedView.ts'
|
||||||
|
|
||||||
interface AuthorPageParams {
|
interface AuthorPageParams {
|
||||||
postsService: PostsService
|
postsService: PostsService
|
||||||
|
@ -17,5 +20,11 @@ export default function AuthorPage({ postsService }: AuthorPageParams) {
|
||||||
[postsService, username],
|
[postsService, username],
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FeedView loadMore={fetchPosts} />
|
const { pages, loadNextPage } = useFeedViewModel(fetchPosts)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SingleColumnLayout navbar={<NavBar />}>
|
||||||
|
<FeedView pages={pages} onLoadMore={loadNextPage} />
|
||||||
|
</SingleColumnLayout>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ import { PostsService } from '../model/posts/postsService.ts'
|
||||||
import { useUserStore } from '../store/userStore.ts'
|
import { useUserStore } from '../store/userStore.ts'
|
||||||
import { MediaService } from '../model/mediaService.ts'
|
import { MediaService } from '../model/mediaService.ts'
|
||||||
import NewPostWidget from '../components/NewPostWidget.tsx'
|
import NewPostWidget from '../components/NewPostWidget.tsx'
|
||||||
import { useFeedViewModel } from '../feed/feedViewModel.ts'
|
import { useFeedViewModel } from '../feed/FeedView.ts'
|
||||||
import { Post } from '../model/posts/posts.ts'
|
import { Post } from '../model/posts/posts.ts'
|
||||||
import { Temporal } from '@js-temporal/polyfill'
|
import { Temporal } from '@js-temporal/polyfill'
|
||||||
import AppLayout from '../layouts/AppLayout.tsx'
|
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
|
||||||
|
import NavBar from '../components/NavBar.tsx'
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
postsService: PostsService
|
postsService: PostsService
|
||||||
|
@ -26,7 +27,7 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
|
||||||
[postsService],
|
[postsService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [pages, setPages, loadNextPage] = useFeedViewModel(fetchPosts)
|
const { pages, setPages, loadNextPage } = useFeedViewModel(fetchPosts)
|
||||||
|
|
||||||
const onCreatePost = useCallback(
|
const onCreatePost = useCallback(
|
||||||
async (content: string, files: File[]) => {
|
async (content: string, files: File[]) => {
|
||||||
|
@ -49,11 +50,11 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<SingleColumnLayout navbar={<NavBar />}>
|
||||||
<main className={`w-full max-w-3xl mx-auto`}>
|
<main className={`w-full max-w-3xl mx-auto`}>
|
||||||
<NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />
|
<NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />
|
||||||
<FeedView pages={pages} onLoadMore={loadNextPage} />
|
<FeedView pages={pages} onLoadMore={loadNextPage} />
|
||||||
</main>
|
</main>
|
||||||
</AppLayout>
|
</SingleColumnLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +1,98 @@
|
||||||
import { useParams } from 'react-router'
|
import { useParams } from 'react-router'
|
||||||
import { useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
|
||||||
|
import TextInput from '../components/TextInput.tsx'
|
||||||
|
import PrimaryButton from '../components/PrimaryButton.tsx'
|
||||||
|
import PrimaryLinkButton from '../components/PrimaryLinkButton.tsx'
|
||||||
|
|
||||||
|
const SignupCodeKey = 'signupCode'
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
const { code: signupCode } = useParams()
|
const { code } = useParams()
|
||||||
|
const [signupCode, setSignupCode] = useState<string | null>(null)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
|
const dialogRef = useRef<HTMLDialogElement | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (signupCode) return
|
||||||
|
|
||||||
|
let theSignupCode: string | null
|
||||||
|
if (code) {
|
||||||
|
theSignupCode = code
|
||||||
|
setSignupCode(theSignupCode)
|
||||||
|
localStorage.setItem(SignupCodeKey, theSignupCode)
|
||||||
|
} else {
|
||||||
|
theSignupCode = localStorage.getItem(SignupCodeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theSignupCode) {
|
||||||
|
dialogRef.current?.showModal()
|
||||||
|
}
|
||||||
|
}, [code, signupCode])
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
}
|
|
||||||
|
|
||||||
if (!signupCode) {
|
try {
|
||||||
return <RejectionMessage />
|
// todo
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="w-full max-w-3xl mx-auto p-4">
|
<SingleColumnLayout>
|
||||||
|
<main className="w-full mx-auto p-4">
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
|
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="username" className="text-sm text-gray-600">
|
<label htmlFor="username" className="text-sm text-gray-600">
|
||||||
Username
|
username
|
||||||
</label>
|
</label>
|
||||||
<input
|
<TextInput id="username" value={username} onInput={setUsername} required />
|
||||||
id="username"
|
|
||||||
type="text"
|
|
||||||
className="p-2 border rounded bg-white/50 focus:outline-none focus:border-gray-400"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="email" className="text-sm text-gray-600">
|
<label htmlFor="email" className="text-sm text-gray-600">
|
||||||
Email (optional)
|
email (optional)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<TextInput id="email" value={email} onInput={setEmail} required />
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
className="p-2 border rounded bg-white/50 focus:outline-none focus:border-gray-400"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="password" className="text-sm text-gray-600">
|
<label htmlFor="password" className="text-sm text-gray-600">
|
||||||
Password
|
password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<TextInput
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
className="p-2 border rounded bg-white/50 focus:outline-none focus:border-gray-400"
|
value={password}
|
||||||
|
onInput={setPassword}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<PrimaryButton className="mt-4" disabled={isSubmitting} type="submit">
|
||||||
type="submit"
|
{isSubmitting ? 'wait...' : 'give me an account pls'}
|
||||||
disabled={isSubmitting}
|
</PrimaryButton>
|
||||||
className="mt-4 p-2 bg-gray-800 text-white rounded hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Creating account...' : 'Create account'}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RejectionMessage() {
|
<dialog
|
||||||
return (
|
id="go-away-dialog"
|
||||||
<main className="w-full max-w-3xl mx-auto p-4">
|
ref={dialogRef}
|
||||||
<div className="mt-12 text-gray-600 flex flex-col gap-2">
|
className="p-6 rounded-lg shadow-lg m-auto outline-none"
|
||||||
<p>An invitation is required to create an account.</p>
|
>
|
||||||
|
<div className="text-gray-600 flex flex-col gap-2">
|
||||||
|
<h1 className={`font-bold text-lg`}>STOP !!!</h1>
|
||||||
|
<p>You need an invitation to sign up</p>
|
||||||
<p>
|
<p>
|
||||||
I'm surprised you even found your way here without one and honestly I'd prefer it if you
|
I'm surprised you even found your way here without one and honestly I'd prefer it if you
|
||||||
would leave
|
would leave
|
||||||
|
@ -79,7 +101,11 @@ function RejectionMessage() {
|
||||||
If you <span className="italic">do</span> want to create an account, you should know who
|
If you <span className="italic">do</span> want to create an account, you should know who
|
||||||
to contact
|
to contact
|
||||||
</p>
|
</p>
|
||||||
|
<PrimaryLinkButton className={`mt-4`} href="https://en.wikipedia.org/wiki/Special:Random">
|
||||||
|
I'm sorry I'll go somewhere else :(
|
||||||
|
</PrimaryLinkButton>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</dialog>
|
||||||
|
</SingleColumnLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
14
src/utils/openFileDialog.ts
Normal file
14
src/utils/openFileDialog.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export function openFileDialog(accept: string, multiple: boolean): Promise<FileList | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const inputEl = document.createElement('input')
|
||||||
|
inputEl.type = 'file'
|
||||||
|
inputEl.accept = accept
|
||||||
|
inputEl.multiple = multiple
|
||||||
|
|
||||||
|
inputEl.addEventListener('change', () => {
|
||||||
|
resolve(inputEl.files)
|
||||||
|
})
|
||||||
|
|
||||||
|
inputEl.click()
|
||||||
|
})
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue