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,11 +1,10 @@
import { Link } from 'react-router'
import NavLinkButton from './NavLinkButton'
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 className={`w-full flex flex-row-reverse gap-4 px-4 md:px-8 py-0.5`}>
<NavLinkButton to="/signup">register</NavLinkButton>
<NavLinkButton to="/login">login</NavLinkButton>
</nav>
)
}

View file

@ -0,0 +1,13 @@
import { PropsWithChildren } from 'react'
import { Link } from 'react-router'
interface NavLinkButtonProps {
to: string
}
export default function NavLinkButton({ to, children }: PropsWithChildren<NavLinkButtonProps>) {
return (
<Link className={`text-primary-500`} to={to}>
{children}
</Link>
)
}

View file

@ -5,7 +5,7 @@ import SecondaryButton from './SecondaryButton.tsx'
import { openFileDialog } from '../utils/openFileDialog.ts'
interface NewPostWidgetProps {
onSubmit: (content: string, media: File[]) => void
onSubmit: (content: string, media: { file: File; width: number; height: number }[]) => void
isSubmitting?: boolean
}
@ -13,6 +13,8 @@ interface Attachment {
id: string
file: File
objectUrl: string
width: number
height: number
}
export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPostWidgetProps) {
@ -29,13 +31,8 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
return
}
const newFiles = Array.from(files).map((file) => ({
id: crypto.randomUUID(),
file,
objectUrl: URL.createObjectURL(file),
}))
setAttachments((attachments) => [...attachments, ...newFiles])
const newAttachments = await Promise.all(Array.from(files).map(createAttachment))
setAttachments((attachments) => [...attachments, ...newAttachments])
}
const handleSubmit = () => {
@ -43,10 +40,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
return
}
onSubmit(
content,
attachments.map(({ file }) => file),
)
onSubmit(content, attachments)
attachments.forEach(({ objectUrl }) => URL.revokeObjectURL(objectUrl))
setContent('')
@ -91,7 +85,7 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
</div>
)}
<div className="flex justify-between items-center">
<div className="flex justify-between items-center pt-2">
<SecondaryButton onClick={onAddMediaClicked}>+ add media</SecondaryButton>
<PrimaryButton
onClick={handleSubmit}
@ -103,3 +97,33 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos
</div>
)
}
async function createAttachment(file: File): Promise<Attachment> {
if (!file.type.startsWith('image/')) {
throw new Error('not an image')
}
const objectUrl = URL.createObjectURL(file)
const { width, height } = await getImageFileDimensions(objectUrl)
console.debug('width', width, 'height', height)
return {
id: crypto.randomUUID(),
file,
objectUrl,
width,
height,
}
}
function getImageFileDimensions(objectURL: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
const img = document.createElement('img')
img.addEventListener('load', () => {
resolve({ width: img.width, height: img.height })
})
img.src = objectURL
})
}

View file

@ -1,60 +0,0 @@
import { Post } from '../model/posts/posts.ts'
import { Link } from 'react-router'
import { useEffect, useState } from 'react'
interface PostItemProps {
post: Post
index: number
}
export default function PostItem({ post, index }: PostItemProps) {
const formattedDate = post.createdAt.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
const [visible, setVisible] = useState(false)
useEffect(() => {
const timeout = setTimeout(() => setVisible(true))
return () => clearTimeout(timeout)
}, [])
const opacity = visible ? 'opacity-100' : 'opacity-0'
const delayMs = index * 100
return (
<article
className={`w-full p-4 ${opacity} transition-opacity duration-500`}
style={{ transitionDelay: `${delayMs}ms` }}
key={post.postId}
>
<div className="text-sm text-gray-500 mb-3">
<Link to={`/u/${post.authorName}`} className="text-gray-400 hover:underline mr-2">
@{post.authorName}
</Link>
{formattedDate}
</div>
<div className="text-gray-800 mb-4 whitespace-pre-wrap">{post.content}</div>
{post.media.length > 0 && (
<div className="grid gap-4 grid-cols-1">
{post.media.map((src) => (
<img
key={src.toString()}
src={src.toString()}
alt="todo sry :("
className="w-full h-auto"
loading="lazy"
/>
))}
</div>
)}
</article>
)
}

View file

@ -1,20 +0,0 @@
import { Post } from '../model/posts/posts.ts'
import PostItem from './PostItem'
interface PostsFeedProps {
pages: Post[][]
}
export default function PostsList({ pages }: PostsFeedProps) {
return <div className="flex flex-col gap-6 w-full">{pages.map(renderPage)}</div>
}
function renderPage(posts: Post[]) {
return (
<div className="flex flex-col gap-6 w-full">
{posts.map((post, idx) => (
<PostItem key={post.postId} post={post} index={idx} />
))}
</div>
)
}

View file

@ -1,4 +1,7 @@
import { Ref } from 'react'
interface TextInputProps {
ref?: Ref<HTMLInputElement>
id?: string
type?: 'text' | 'email' | 'password'
value: string
@ -10,6 +13,7 @@ interface TextInputProps {
export default function TextInput({
id,
ref,
value,
onInput,
className: extraClasses = '',
@ -19,6 +23,7 @@ export default function TextInput({
}: TextInputProps) {
return (
<input
ref={ref}
id={id}
value={value}
type={type}