This commit is contained in:
john 2025-05-18 13:41:08 +02:00
parent abd7c2f073
commit 384da1e832
18 changed files with 150 additions and 116 deletions

View file

@ -9,7 +9,6 @@ import { AuthService } from './app/auth/authService.ts'
import { useUser } from './app/user/userStore.ts' import { useUser } from './app/user/userStore.ts'
import LogoutPage from './app/auth/pages/LogoutPage.tsx' import LogoutPage from './app/auth/pages/LogoutPage.tsx'
import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx'
import Protected from './app/auth/components/Protected.tsx'
function App() { function App() {
const { user } = useUser() const { user } = useUser()
@ -21,13 +20,11 @@ function App() {
<BrowserRouter> <BrowserRouter>
<UnauthorizedHandler> <UnauthorizedHandler>
<Routes> <Routes>
<Route element={<Protected />}> <Route
<Route path={'/'}
path={'/'} element={<HomePage postsService={postService} mediaService={mediaService} />}
element={<HomePage postsService={postService} mediaService={mediaService} />} />
/> <Route path="/u/:username" element={<AuthorPage postsService={postService} />} />
<Route path="/u/:username" element={<AuthorPage postsService={postService} />} />
</Route>
<Route path="/login" element={<LoginPage authService={authService} />} /> <Route path="/login" element={<LoginPage authService={authService} />} />
<Route path="/logout" element={<LogoutPage authService={authService} />} /> <Route path="/logout" element={<LogoutPage authService={authService} />} />
<Route path="/signup/:code?" element={<SignupPage authService={authService} />} /> <Route path="/signup/:code?" element={<SignupPage authService={authService} />} />

View file

@ -275,6 +275,7 @@ export interface components {
authorId: string authorId: string
content: string content: string
media: components['schemas']['CreatePostRequestMedia'][] media: components['schemas']['CreatePostRequestMedia'][]
isPublic: boolean | null
} }
CreatePostRequestMedia: { CreatePostRequestMedia: {
/** Format: uuid */ /** Format: uuid */
@ -292,7 +293,7 @@ export interface components {
postId: string postId: string
} }
GetAllPublicPostsResponse: { GetAllPublicPostsResponse: {
posts: components['schemas']['PublicPostDto'][] posts: components['schemas']['PostDto'][]
/** Format: uuid */ /** Format: uuid */
next: string | null next: string | null
} }
@ -305,21 +306,21 @@ export interface components {
userId: string userId: string
username: string username: string
} }
PublicPostAuthorDto: { PostAuthorDto: {
/** Format: uuid */ /** Format: uuid */
authorId: string authorId: string
username: string username: string
} }
PublicPostDto: { PostDto: {
author: components['schemas']['PublicPostAuthorDto'] author: components['schemas']['PostAuthorDto']
/** Format: uuid */ /** Format: uuid */
postId: string postId: string
content: string content: string
media: components['schemas']['PublicPostMediaDto'][] media: components['schemas']['PostMediaDto'][]
/** Format: date-time */ /** Format: date-time */
createdAt: string createdAt: string
} }
PublicPostMediaDto: { PostMediaDto: {
/** Format: uri */ /** Format: uri */
url: string url: string
/** Format: int32 */ /** Format: int32 */

View file

@ -0,0 +1,30 @@
import { useUser } from '../../user/userStore.ts'
import NavButton from '../../../components/buttons/NavButton.tsx'
import { useLocation } from 'react-router-dom'
export default function AuthNavButtons() {
const { user } = useUser()
const { pathname } = useLocation()
const redirectQuery = new URLSearchParams()
redirectQuery.set('t', pathname)
const loggedIn = user != null
if (loggedIn) {
return (
<>
<NavButton to="/logout">logout</NavButton>
</>
)
} else {
const search = redirectQuery.toString()
return (
<>
<NavButton to={{ pathname: '/login', search }}>login</NavButton>
<NavButton to={{ pathname: '/signup', search }}>register</NavButton>
</>
)
}
}

View file

@ -1,11 +1,13 @@
import { useRef, useState, FormEvent, useEffect } from 'react' import { useRef, useState, FormEvent, useEffect } from 'react'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import TextInput from '../../../components/TextInput.tsx' import TextInput from '../../../components/inputs/TextInput.tsx'
import PrimaryButton from '../../../components/PrimaryButton.tsx' import Button from '../../../components/buttons/Button.tsx'
import { AuthService } from '../authService.ts' import { AuthService } from '../authService.ts'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import SecondaryNavButton from '../../../components/SecondaryNavButton.tsx'
import { useUser } from '../../user/userStore.ts' import { useUser } from '../../user/userStore.ts'
import NavBar from '../../../components/NavBar.tsx'
import NavButton from '../../../components/buttons/NavButton.tsx'
import LinkButton from '../../../components/buttons/LinkButton.tsx'
interface LoginPageProps { interface LoginPageProps {
authService: AuthService authService: AuthService
@ -24,7 +26,8 @@ export default function LoginPage({ authService }: LoginPageProps) {
useEffect(() => { useEffect(() => {
if (user) { if (user) {
navigate('/') const search = new URLSearchParams(window.location.search)
navigate(search.get('t') || '/')
} }
}, [user, navigate]) }, [user, navigate])
@ -55,7 +58,13 @@ export default function LoginPage({ authService }: LoginPageProps) {
} }
return ( return (
<SingleColumnLayout> <SingleColumnLayout
navbar={
<NavBar>
<NavButton to={'/'}>home</NavButton>
</NavBar>
}
>
<main className="w-full mx-auto p-4"> <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}>
@ -86,11 +95,13 @@ export default function LoginPage({ authService }: LoginPageProps) {
/> />
</div> </div>
<PrimaryButton className="mt-4" disabled={isSubmitting} type="submit"> <Button className="mt-4" disabled={isSubmitting} type="submit">
{isSubmitting ? 'wait...' : 'make login pls'} {isSubmitting ? 'wait...' : 'make login pls'}
</PrimaryButton> </Button>
<SecondaryNavButton to={'/signup'}>register instead?</SecondaryNavButton> <LinkButton secondary to={{ pathname: '/signup', search: window.location.search }}>
register instead?
</LinkButton>
<span className="text-xs h-3 text-red-500">{error}</span> <span className="text-xs h-3 text-red-500">{error}</span>
</form> </form>

View file

@ -1,12 +1,14 @@
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react' import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import TextInput from '../../../components/TextInput.tsx' import TextInput from '../../../components/inputs/TextInput.tsx'
import PrimaryButton from '../../../components/PrimaryButton.tsx' import Button from '../../../components/buttons/Button.tsx'
import PrimaryLinkButton from '../../../components/PrimaryLinkButton.tsx' import AnchorButton from '../../../components/buttons/AnchorButton.tsx'
import { invalid, valid, Validation } from '../../../utils/validation.ts' import { invalid, valid, Validation } from '../../../utils/validation.ts'
import { AuthService } from '../authService.ts' import { AuthService } from '../authService.ts'
import SecondaryNavButton from '../../../components/SecondaryNavButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx'
import NavBar from '../../../components/NavBar.tsx'
import NavButton from '../../../components/buttons/NavButton.tsx'
const SignupCodeKey = 'signupCode' const SignupCodeKey = 'signupCode'
@ -82,7 +84,13 @@ export default function SignupPage({ authService }: SignupPageProps) {
} }
return ( return (
<SingleColumnLayout> <SingleColumnLayout
navbar={
<NavBar>
<NavButton to={'/'}>home</NavButton>
</NavBar>
}
>
<main className="w-full mx-auto p-4"> <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}>
@ -102,14 +110,16 @@ export default function SignupPage({ authService }: SignupPageProps) {
type="password" type="password"
ref={passwordInputRef} ref={passwordInputRef}
/> />
<PrimaryButton <Button
className="mt-4" className="mt-4"
disabled={isSubmitting || !!usernameError || !!passwordError} disabled={isSubmitting || !!usernameError || !!passwordError}
type="submit" type="submit"
> >
{isSubmitting ? 'wait...' : 'give me an account pls'} {isSubmitting ? 'wait...' : 'give me an account pls'}
</PrimaryButton> </Button>
<SecondaryNavButton to={'/login'}>login instead?</SecondaryNavButton> <LinkButton secondary to={'/login'}>
login instead?
</LinkButton>
</form> </form>
</div> </div>
</main> </main>
@ -130,9 +140,9 @@ export default function SignupPage({ authService }: SignupPageProps) {
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"> <AnchorButton className={`mt-4`} href="https://en.wikipedia.org/wiki/Special:Random">
I'm sorry I'll go somewhere else :( I'm sorry I'll go somewhere else :(
</PrimaryLinkButton> </AnchorButton>
</div> </div>
</dialog> </dialog>
</SingleColumnLayout> </SingleColumnLayout>

View file

@ -5,7 +5,8 @@ import { useParams } from 'react-router-dom'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import NavBar from '../../../components/NavBar.tsx' import NavBar from '../../../components/NavBar.tsx'
import { useFeedViewModel } from '../components/FeedView.ts' import { useFeedViewModel } from '../components/FeedView.ts'
import NavLinkButton from '../../../components/NavLinkButton.tsx' import NavButton from '../../../components/buttons/NavButton.tsx'
import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx'
interface AuthorPageParams { interface AuthorPageParams {
postsService: PostsService postsService: PostsService
@ -27,8 +28,8 @@ export default function AuthorPage({ postsService }: AuthorPageParams) {
<SingleColumnLayout <SingleColumnLayout
navbar={ navbar={
<NavBar> <NavBar>
<NavLinkButton to={'/'}>home</NavLinkButton> <NavButton to={'/'}>home</NavButton>
<NavLinkButton to="/logout">logout</NavLinkButton> <AuthNavButtons />
</NavBar> </NavBar>
} }
> >

View file

@ -9,7 +9,7 @@ import { Post } from '../posts/posts.ts'
import { Temporal } from '@js-temporal/polyfill' import { Temporal } from '@js-temporal/polyfill'
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
import NavBar from '../../../components/NavBar.tsx' import NavBar from '../../../components/NavBar.tsx'
import NavLinkButton from '../../../components/NavLinkButton.tsx' import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx'
interface HomePageProps { interface HomePageProps {
postsService: PostsService postsService: PostsService
@ -18,7 +18,6 @@ interface HomePageProps {
export default function HomePage({ postsService, mediaService }: HomePageProps) { export default function HomePage({ postsService, mediaService }: HomePageProps) {
const { user } = useUser() const { user } = useUser()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const fetchPosts = useCallback( const fetchPosts = useCallback(
@ -59,16 +58,18 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
[mediaService, postsService, setPages, user], [mediaService, postsService, setPages, user],
) )
const isLoggedIn = user != null
return ( return (
<SingleColumnLayout <SingleColumnLayout
navbar={ navbar={
<NavBar> <NavBar>
<NavLinkButton to="/logout">logout</NavLinkButton> <AuthNavButtons />
</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} /> {isLoggedIn && <NewPostWidget onSubmit={onCreatePost} isSubmitting={isSubmitting} />}
<FeedView pages={pages} onLoadMore={loadNextPage} /> <FeedView pages={pages} onLoadMore={loadNextPage} />
</main> </main>
</SingleColumnLayout> </SingleColumnLayout>

View file

@ -4,7 +4,12 @@ import client from '../../api/client.ts'
export class PostsService { export class PostsService {
constructor() {} constructor() {}
async createNew(authorId: string, content: string, media: CreatePostMedia[]): Promise<string> { async createNew(
authorId: string,
content: string,
media: CreatePostMedia[],
isPublic: boolean,
): Promise<string> {
const response = await client.POST('/posts', { const response = await client.POST('/posts', {
body: { body: {
authorId, authorId,
@ -12,6 +17,7 @@ export class PostsService {
media: media.map((m) => { media: media.map((m) => {
return { ...m, type: null, url: m.url.toString() } return { ...m, type: null, url: m.url.toString() }
}), }),
isPublic,
}, },
credentials: 'include', credentials: 'include',
}) })

View file

@ -1,7 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import FancyTextEditor, { TextInputKeyDownEvent } from './FancyTextEditor.tsx' import FancyTextEditor, { TextInputKeyDownEvent } from './inputs/FancyTextEditor.tsx'
import PrimaryButton from './PrimaryButton.tsx' import Button from './buttons/Button.tsx'
import SecondaryButton from './SecondaryButton.tsx'
import { openFileDialog } from '../utils/openFileDialog.ts' import { openFileDialog } from '../utils/openFileDialog.ts'
interface NewPostWidgetProps { interface NewPostWidgetProps {
@ -86,13 +85,15 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
)} )}
<div className="flex justify-between items-center pt-2"> <div className="flex justify-between items-center pt-2">
<SecondaryButton onClick={onAddMediaClicked}>+ add media</SecondaryButton> <Button secondary onClick={onAddMediaClicked}>
<PrimaryButton + add media
</Button>
<Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)} disabled={isSubmitting || (content.trim() === '' && attachments.length === 0)}
> >
post post
</PrimaryButton> </Button>
</div> </div>
</div> </div>
) )

View file

@ -1,27 +0,0 @@
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={`secondary-button ${extraClasses}
`}
>
{children}
</button>
)
}

View file

@ -1,18 +0,0 @@
import { PropsWithChildren } from 'react'
interface SecondaryLinkButtonProps {
href: string
className?: string
}
export default function SecondaryLinkButton({
href,
className: extraClasses = '',
children,
}: PropsWithChildren<SecondaryLinkButtonProps>) {
return (
<a href={href} className={`secondary-button text-center ${extraClasses}`}>
{children}
</a>
)
}

View file

@ -1,19 +0,0 @@
import { PropsWithChildren } from 'react'
import { Link } from 'react-router-dom'
interface SecondaryNavButtonProps {
to: string
className?: string
}
export default function SecondaryNavButton({
to,
className: extraClasses = '',
children,
}: PropsWithChildren<SecondaryNavButtonProps>) {
return (
<Link to={to} className={`secondary-button text-center ${extraClasses}`}>
{children}
</Link>
)
}

View file

@ -3,15 +3,18 @@ import { PropsWithChildren } from 'react'
interface PrimaryLinkButtonProps { interface PrimaryLinkButtonProps {
href: string href: string
className?: string className?: string
secondary?: boolean
} }
export default function PrimaryLinkButton({ export default function AnchorButton({
href, href,
className: extraClasses = '', className: extraClasses = '',
children, children,
secondary = false,
}: PropsWithChildren<PrimaryLinkButtonProps>) { }: PropsWithChildren<PrimaryLinkButtonProps>) {
const klass = secondary ? 'secondary-button' : 'primary-button'
return ( return (
<a href={href} className={`primary-button text-center ${extraClasses}`}> <a href={href} className={`${klass} text-center ${extraClasses}`}>
{children} {children}
</a> </a>
) )

View file

@ -5,21 +5,24 @@ interface PrimaryButtonProps {
type?: 'submit' | 'button' type?: 'submit' | 'button'
onClick?: () => void onClick?: () => void
className?: string className?: string
secondary?: boolean
} }
export default function PrimaryButton({ export default function Button({
disabled = false, disabled = false,
type = 'button', type = 'button',
onClick = () => {}, onClick = () => {},
className: extraClasses = '', className: extraClasses = '',
children, children,
secondary = false,
}: PropsWithChildren<PrimaryButtonProps>) { }: PropsWithChildren<PrimaryButtonProps>) {
const klass = secondary ? 'secondary-button' : 'primary-button'
return ( return (
<button <button
type={type} type={type}
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
className={`primary-button ${extraClasses}`} className={`${klass} ${extraClasses}`}
> >
{children} {children}
</button> </button>

View file

@ -0,0 +1,28 @@
import { PropsWithChildren } from 'react'
import { Link } from 'react-router-dom'
interface LinkButtonProps {
to: string | To
className?: string
secondary?: boolean
}
interface To {
pathname: string
search?: string
hash?: string
}
export default function LinkButton({
to,
className: extraClasses = '',
secondary = false,
children,
}: PropsWithChildren<LinkButtonProps>) {
const klass = secondary ? 'secondary-button' : 'primary-button'
return (
<Link to={to} className={`${klass} text-center ${extraClasses}`}>
{children}
</Link>
)
}

View file

@ -1,10 +1,16 @@
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
interface NavLinkButtonProps { interface NavLinkButtonProps {
to: string to: string | To
} }
export default function NavLinkButton({ to, children }: PropsWithChildren<NavLinkButtonProps>) { interface To {
pathname: string
search?: string
hash?: string
}
export default function NavButton({ to, children }: PropsWithChildren<NavLinkButtonProps>) {
return ( return (
<Link className={`text-primary-500`} to={to}> <Link className={`text-primary-500`} to={to}>
{children} {children}