ligun and sogup

This commit is contained in:
john 2025-05-06 16:31:55 +02:00
parent b6633d6f25
commit 4573048a47
24 changed files with 482 additions and 226 deletions

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react'
import FeedView from '../feed/FeedView.tsx'
import { PostsService } from '../model/posts/postsService.ts'
import { PostsService } from '../feed/models/posts/postsService.ts'
import { useParams } from 'react-router'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import NavBar from '../components/NavBar.tsx'

View file

@ -1,11 +1,11 @@
import { useCallback, useState } from 'react'
import FeedView from '../feed/FeedView.tsx'
import { PostsService } from '../model/posts/postsService.ts'
import { PostsService } from '../feed/models/posts/postsService.ts'
import { useUserStore } from '../store/userStore.ts'
import { MediaService } from '../model/mediaService.ts'
import { MediaService } from '../model/media/mediaService.ts'
import NewPostWidget from '../components/NewPostWidget.tsx'
import { useFeedViewModel } from '../feed/FeedView.ts'
import { Post } from '../model/posts/posts.ts'
import { Post } from '../feed/models/posts/posts.ts'
import { Temporal } from '@js-temporal/polyfill'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import NavBar from '../components/NavBar.tsx'
@ -30,15 +30,25 @@ export default function HomePage({ postsService, mediaService }: HomePageProps)
const { pages, setPages, loadNextPage } = useFeedViewModel(fetchPosts)
const onCreatePost = useCallback(
async (content: string, files: File[]) => {
if (!onCreatePost) return
async (content: string, files: { file: File; width: number; height: number }[]) => {
setIsSubmitting(true)
if (user == null) throw new Error('Not logged in')
try {
if (user == null) throw new Error('Not logged in')
const urls = await Promise.all(files.map((file) => mediaService.uploadFile(file)))
const postId = await postsService.createNew(user.userId, content, urls)
const post = new Post(postId, content, urls, Temporal.Now.instant(), user.username)
const media = await Promise.all(
files.map(async ({ file, width, height }) => {
console.debug('do mediaService.uploadFile', file, 'width', width, 'height', height)
const { mediaId, url } = await mediaService.uploadFile(file)
return {
mediaId,
url,
width,
height,
}
}),
)
const postId = await postsService.createNew(user.userId, content, media)
const post = new Post(postId, content, media, Temporal.Now.instant(), user.username)
setPages((pages) => [[post], ...pages])
} catch (error) {
console.error('Failed to create post:', error)

85
src/pages/LoginPage.tsx Normal file
View file

@ -0,0 +1,85 @@
import { useRef, useState, FormEvent } from 'react'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import TextInput from '../components/TextInput.tsx'
import PrimaryButton from '../components/PrimaryButton.tsx'
import { AuthService } from '../auth/authService.ts'
interface LoginPageProps {
authService: AuthService
}
export default function LoginPage({ authService }: LoginPageProps) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const usernameInputRef = useRef<HTMLInputElement | null>(null)
const passwordInputRef = useRef<HTMLInputElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!username) {
setError("you didn't username D:<")
return
}
if (!password) {
setError("you didn't password D:<")
return
}
setError(null)
setIsSubmitting(true)
try {
const loginResult = await authService.login(username, password)
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'something went terribly wrong')
} finally {
setIsSubmitting(false)
}
}
return (
<SingleColumnLayout>
<main className="w-full mx-auto p-4">
<div className="mt-12">
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
<div className="flex flex-col gap-1">
<label htmlFor="username" className="text-sm text-gray-600">
username
</label>
<TextInput
ref={usernameInputRef}
id="username"
value={username}
onInput={setUsername}
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="password" className="text-sm text-gray-600">
password
</label>
<TextInput
ref={passwordInputRef}
type="password"
id="password"
value={password}
onInput={setPassword}
/>
</div>
<PrimaryButton className="mt-4" disabled={isSubmitting} type="submit">
{isSubmitting ? 'wait...' : 'make login pls'}
</PrimaryButton>
<span className="text-xs h-3 text-red-500">{error}</span>
</form>
</div>
</main>
</SingleColumnLayout>
)
}

View file

@ -1,9 +1,10 @@
import { useParams } from 'react-router'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState, FormEvent, useCallback, Ref } 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'
import { invalid, valid, Validation } from '../utils/validation.ts'
const SignupCodeKey = 'signupCode'
@ -12,9 +13,17 @@ export default function SignupPage() {
const [signupCode, setSignupCode] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [username, setUsername] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [username, setUsername, usernameError, validateUsername] =
useValidatedInput(isValidUsername)
const [email, setEmail, emailError, validateEmail] = useValidatedInput(isValidEmail)
const [password, setPassword, passwordError, validatePassword] =
useValidatedInput(isValidPassword)
const userNameInputRef = useRef<HTMLInputElement | null>(null)
const emailInputRef = useRef<HTMLInputElement | null>(null)
const passwordInputRef = useRef<HTMLInputElement | null>(null)
const dialogRef = useRef<HTMLDialogElement | null>(null)
@ -35,8 +44,29 @@ export default function SignupPage() {
}
}, [code, signupCode])
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const isUsernameValid = validateUsername()
const isEmailValid = validateEmail()
const isPasswordValid = validatePassword()
if (!isPasswordValid) {
passwordInputRef.current?.focus()
}
if (!isEmailValid) {
emailInputRef.current?.focus()
}
if (!isUsernameValid) {
userNameInputRef.current?.focus()
}
if (!isUsernameValid || !isEmailValid || !isPasswordValid) {
return
}
setIsSubmitting(true)
try {
@ -51,34 +81,33 @@ export default function SignupPage() {
<main className="w-full mx-auto p-4">
<div className="mt-12">
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
<div className="flex flex-col gap-2">
<label htmlFor="username" className="text-sm text-gray-600">
username
</label>
<TextInput id="username" value={username} onInput={setUsername} required />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="email" className="text-sm text-gray-600">
email (optional)
</label>
<TextInput id="email" value={email} onInput={setEmail} required />
</div>
<div className="flex flex-col gap-2">
<label htmlFor="password" className="text-sm text-gray-600">
password
</label>
<TextInput
id="password"
type="password"
value={password}
onInput={setPassword}
required
/>
</div>
<PrimaryButton className="mt-4" disabled={isSubmitting} type="submit">
<FormInput
id="username"
value={username}
onInput={setUsername}
error={usernameError}
ref={userNameInputRef}
/>
<FormInput
id="email"
value={email}
onInput={setEmail}
error={emailError}
ref={emailInputRef}
/>
<FormInput
id="password"
value={password}
onInput={setPassword}
error={passwordError}
type="password"
ref={passwordInputRef}
/>
<PrimaryButton
className="mt-4"
disabled={isSubmitting || !!usernameError || !!passwordError}
type="submit"
>
{isSubmitting ? 'wait...' : 'give me an account pls'}
</PrimaryButton>
</form>
@ -109,3 +138,87 @@ export default function SignupPage() {
</SingleColumnLayout>
)
}
interface FormInputProps {
id: string
value: string
onInput: (value: string) => void
error: string | null
type?: 'text' | 'password'
ref: Ref<HTMLInputElement>
}
function FormInput({ id, value, onInput, error, type = 'text', ref }: FormInputProps) {
return (
<div className="flex flex-col gap-1">
<label htmlFor={id} className="text-sm text-gray-600">
{id}
</label>
<TextInput ref={ref} type={type} id={id} value={value} onInput={onInput} />
<span className="text-xs h-3 text-red-500">{error}</span>
</div>
)
}
type UseValidateInputReturn = [string, (value: string) => void, string | null, () => boolean]
function useValidatedInput(validator: (value: string) => Validation): UseValidateInputReturn {
const [value, setValue] = useState<string>('')
const [error, setError] = useState<string | null>(null)
const [keepValidating, setKeepValidating] = useState(false)
const validate = useCallback(() => {
const { isValid, error } = validator(value)
if (isValid) {
setError(null)
} else {
// We only want to validate on input after they have invalidly submitted once.
// It's annoying if we set error messages before they've even finished typing.
setKeepValidating(true)
setError(error)
}
return isValid
}, [validator, value])
useEffect(() => {
if (keepValidating) {
validate()
}
}, [keepValidating, validate])
return [value, setValue, error, validate]
}
function isValidUsername(username: string): Validation {
if (!username) return invalid('you need to enter a username :/')
if (username.length < 3) {
return invalid('not long enough :(')
}
const usernameRegex = /^[a-zA-Z0-9_-]+$/
if (usernameRegex.test(username)) {
return valid()
} else {
return invalid("that's not a good username :'(")
}
}
function isValidEmail(email: string): Validation {
if (!email) return valid()
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (emailRegex.test(email)) {
return valid()
} else {
return invalid("um sorry but that doesn't look like an email 🤔")
}
}
function isValidPassword(password: string): Validation {
if (password.length >= 10) {
return valid()
} else {
return invalid("that isn't a good password :/")
}
}