From b6633d6f25ee238d91523dbd05641b01f03b362d Mon Sep 17 00:00:00 2001 From: john Date: Tue, 6 May 2025 12:53:32 +0200 Subject: [PATCH] pancy fants --- src/components/FancyTextEditor.tsx | 10 +- src/components/NavBar.tsx | 11 ++ src/components/NewPostWidget.tsx | 33 ++--- src/components/PrimaryButton.tsx | 27 ++++ src/components/PrimaryLinkButton.tsx | 18 +++ src/components/SecondaryButton.tsx | 31 ++++ src/components/TextInput.tsx | 31 ++++ src/feed/{feedViewModel.ts => FeedView.ts} | 2 +- src/index.css | 62 ++++++++ src/layouts/AppLayout.tsx | 15 -- src/layouts/SingleColumnLayout.tsx | 17 +++ src/pages/AuthorPage.tsx | 11 +- src/pages/HomePage.tsx | 11 +- src/pages/SignupPage.tsx | 162 ++++++++++++--------- src/utils/openFileDialog.ts | 14 ++ 15 files changed, 339 insertions(+), 116 deletions(-) create mode 100644 src/components/NavBar.tsx create mode 100644 src/components/PrimaryButton.tsx create mode 100644 src/components/PrimaryLinkButton.tsx create mode 100644 src/components/SecondaryButton.tsx create mode 100644 src/components/TextInput.tsx rename src/feed/{feedViewModel.ts => FeedView.ts} (94%) delete mode 100644 src/layouts/AppLayout.tsx create mode 100644 src/layouts/SingleColumnLayout.tsx create mode 100644 src/utils/openFileDialog.ts diff --git a/src/components/FancyTextEditor.tsx b/src/components/FancyTextEditor.tsx index 6db90f9..f23c28d 100644 --- a/src/components/FancyTextEditor.tsx +++ b/src/components/FancyTextEditor.tsx @@ -18,8 +18,8 @@ export default function FancyTextEditor({ value: _value, onInput, onKeyDown, - className, - placeholder, + className: extraClasses = '', + placeholder = '', }: TextInputProps) { const divRef = useRef(null) const [hasFocus, setHasFocus] = useState(false) @@ -39,7 +39,7 @@ export default function FancyTextEditor({ } if (!value && !hasFocus) { - div.innerText = placeholder ?? '' + div.innerText = placeholder } else if (div.innerText !== value) { div.innerText = value } @@ -58,7 +58,7 @@ export default function FancyTextEditor({ const blurListener = () => { setHasFocus(false) if (!value) { - div.innerText = placeholder ?? '' + div.innerText = placeholder } } @@ -86,7 +86,7 @@ export default function FancyTextEditor({ return (
+ + create account + + + ) +} diff --git a/src/components/NewPostWidget.tsx b/src/components/NewPostWidget.tsx index 20d0165..a70d1c7 100644 --- a/src/components/NewPostWidget.tsx +++ b/src/components/NewPostWidget.tsx @@ -1,5 +1,8 @@ -import { useState, ChangeEvent } from 'react' +import { useState } from 'react' import FancyTextEditor, { TextInputKeyDownEvent } from './FancyTextEditor.tsx' +import PrimaryButton from './PrimaryButton.tsx' +import SecondaryButton from './SecondaryButton.tsx' +import { openFileDialog } from '../utils/openFileDialog.ts' interface NewPostWidgetProps { onSubmit: (content: string, media: File[]) => void @@ -20,20 +23,19 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos setContent(value) } - const handleMediaChange = (e: ChangeEvent) => { - const inputEl = e.target as HTMLInputElement - if (inputEl.files == null || inputEl.files.length === 0) { + async function onAddMediaClicked() { + const files = await openFileDialog('image/*', true) + if (files == null || files.length === 0) { return } - const newFiles = Array.from(inputEl.files).map((file) => ({ + const newFiles = Array.from(files).map((file) => ({ id: crypto.randomUUID(), file, objectUrl: URL.createObjectURL(file), })) setAttachments((attachments) => [...attachments, ...newFiles]) - inputEl.value = '' } const handleSubmit = () => { @@ -90,24 +92,13 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos )}
- - - + post +
) diff --git a/src/components/PrimaryButton.tsx b/src/components/PrimaryButton.tsx new file mode 100644 index 0000000..1430e1e --- /dev/null +++ b/src/components/PrimaryButton.tsx @@ -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) { + return ( + + ) +} diff --git a/src/components/PrimaryLinkButton.tsx b/src/components/PrimaryLinkButton.tsx new file mode 100644 index 0000000..1d3ee8a --- /dev/null +++ b/src/components/PrimaryLinkButton.tsx @@ -0,0 +1,18 @@ +import { PropsWithChildren } from 'react' + +interface PrimaryLinkButtonProps { + href: string + className?: string +} + +export default function PrimaryLinkButton({ + href, + className: extraClasses = '', + children, +}: PropsWithChildren) { + return ( + + {children} + + ) +} diff --git a/src/components/SecondaryButton.tsx b/src/components/SecondaryButton.tsx new file mode 100644 index 0000000..3beafe7 --- /dev/null +++ b/src/components/SecondaryButton.tsx @@ -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) { + return ( + + ) +} diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx new file mode 100644 index 0000000..fecf2a4 --- /dev/null +++ b/src/components/TextInput.tsx @@ -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 ( + onInput(e.target.value)} + placeholder={placeholder} + className={`text-input w-full ${extraClasses}`} + /> + ) +} diff --git a/src/feed/feedViewModel.ts b/src/feed/FeedView.ts similarity index 94% rename from src/feed/feedViewModel.ts rename to src/feed/FeedView.ts index 11f21c3..3e52341 100644 --- a/src/feed/feedViewModel.ts +++ b/src/feed/FeedView.ts @@ -28,5 +28,5 @@ export function useFeedViewModel( } }, [loadMore, hasMore]) - return [pages, setPages, loadNextPage] as const + return { pages, setPages, loadNextPage } as const } diff --git a/src/index.css b/src/index.css index ea6ee36..7288ac8 100644 --- a/src/index.css +++ b/src/index.css @@ -6,4 +6,66 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -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; + } } \ No newline at end of file diff --git a/src/layouts/AppLayout.tsx b/src/layouts/AppLayout.tsx deleted file mode 100644 index 095d06b..0000000 --- a/src/layouts/AppLayout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PropsWithChildren } from 'react' -import { Link } from 'react-router' - -export default function AppLayout({ children }: PropsWithChildren) { - return ( -
- -
{children}
-
- ) -} diff --git a/src/layouts/SingleColumnLayout.tsx b/src/layouts/SingleColumnLayout.tsx new file mode 100644 index 0000000..f1adca3 --- /dev/null +++ b/src/layouts/SingleColumnLayout.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren, ReactNode } from 'react' + +interface SingleColumnLayoutProps { + navbar?: ReactNode +} + +export default function SingleColumnLayout({ + children, + navbar, +}: PropsWithChildren) { + return ( +
+ {navbar} +
{children}
+
+ ) +} diff --git a/src/pages/AuthorPage.tsx b/src/pages/AuthorPage.tsx index b02e19c..e808b24 100644 --- a/src/pages/AuthorPage.tsx +++ b/src/pages/AuthorPage.tsx @@ -2,6 +2,9 @@ import { useCallback } from 'react' import FeedView from '../feed/FeedView.tsx' import { PostsService } from '../model/posts/postsService.ts' 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 { postsService: PostsService @@ -17,5 +20,11 @@ export default function AuthorPage({ postsService }: AuthorPageParams) { [postsService, username], ) - return + const { pages, loadNextPage } = useFeedViewModel(fetchPosts) + + return ( + }> + + + ) } diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 900595b..1a246a4 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -4,10 +4,11 @@ import { PostsService } from '../model/posts/postsService.ts' import { useUserStore } from '../store/userStore.ts' import { MediaService } from '../model/mediaService.ts' 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 { 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 { postsService: PostsService @@ -26,7 +27,7 @@ export default function HomePage({ postsService, mediaService }: HomePageProps) [postsService], ) - const [pages, setPages, loadNextPage] = useFeedViewModel(fetchPosts) + const { pages, setPages, loadNextPage } = useFeedViewModel(fetchPosts) const onCreatePost = useCallback( async (content: string, files: File[]) => { @@ -49,11 +50,11 @@ export default function HomePage({ postsService, mediaService }: HomePageProps) ) return ( - + }>
-
+ ) } diff --git a/src/pages/SignupPage.tsx b/src/pages/SignupPage.tsx index d628930..c38c68a 100644 --- a/src/pages/SignupPage.tsx +++ b/src/pages/SignupPage.tsx @@ -1,85 +1,111 @@ 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() { - const { code: signupCode } = useParams() + const { code } = useParams() + const [signupCode, setSignupCode] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) + const [username, setUsername] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + + const dialogRef = useRef(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) => { e.preventDefault() setIsSubmitting(true) - } - if (!signupCode) { - return + try { + // todo + } finally { + setIsSubmitting(false) + } } return ( -
-
-
-
- - -
+ +
+
+ +
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
- - -
-
- ) -} - -function RejectionMessage() { - return ( -
-
-

An invitation is required to create an account.

-

- I'm surprised you even found your way here without one and honestly I'd prefer it if you - would leave -

-

- If you do want to create an account, you should know who - to contact -

-
-
+ + {isSubmitting ? 'wait...' : 'give me an account pls'} + + +
+
+ + +
+

STOP !!!

+

You need an invitation to sign up

+

+ I'm surprised you even found your way here without one and honestly I'd prefer it if you + would leave +

+

+ If you do want to create an account, you should know who + to contact +

+ + I'm sorry I'll go somewhere else :( + +
+
+ ) } diff --git a/src/utils/openFileDialog.ts b/src/utils/openFileDialog.ts new file mode 100644 index 0000000..66a47f8 --- /dev/null +++ b/src/utils/openFileDialog.ts @@ -0,0 +1,14 @@ +export function openFileDialog(accept: string, multiple: boolean): Promise { + 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() + }) +}