Skip to content

Commit

Permalink
simplify routes
Browse files Browse the repository at this point in the history
  • Loading branch information
nichtsam committed Jan 3, 2025
1 parent 207acef commit 0a7c531
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 242 deletions.
11 changes: 3 additions & 8 deletions app/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import { type NavLink } from '#app/model/nav.ts'

export const mainNav: NavLink[] = [
{
title: 'Translate',
href: '/translate',
title: 'Settings',
href: '/settings',
},
{
title: 'elaborate',
href: '/elaborate',
disabled: true,
},
].filter(({ disabled }) => !disabled)
]
216 changes: 201 additions & 15 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,207 @@
import { Link } from '@remix-run/react'
import { mainNav } from '#app/config/config.ts'
import { getFormProps, getTextareaProps, useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
data,
} from '@remix-run/node'
import { Form, Link, useActionData, useLoaderData } from '@remix-run/react'
import { AlertCircle, ArrowUp, LoaderCircle, Settings2 } from 'lucide-react'
import { z } from 'zod'
import {
Alert,
AlertDescription,
AlertTitle,
} from '#app/components/ui/alert.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Card, CardContent } from '#app/components/ui/card.tsx'
import { ScrollArea } from '#app/components/ui/scroll-area.tsx'
import { type ScreenSizeHandle } from '#app/utils/screen-size.ts'
import { targetLangConfig } from '#app/utils/translation.ts'
import { getSettingsSession, Translator } from '#app/utils/translator.server.ts'
import { useIsPending } from '#app/utils/ui.ts'

export const schema = z.object({
expression: z
.string({
required_error: 'Expression is required for an polyglotization!',
})
.max(120, 'Expression is limited to 120 characters.'),
})

export const handle: ScreenSizeHandle = {
screenSize: true,
}

export const loader = async ({ request }: LoaderFunctionArgs) => {
const settings = await getSettingsSession(request)

return {
valid: !!settings.data.targetLanguages,
}
}

export const action = async ({ request }: ActionFunctionArgs) => {
const translationSession = await getSettingsSession(request)
const sourceLanguage = translationSession.get('sourceLanguage')
const targetLanguages = translationSession.get('targetLanguages')

if (!targetLanguages) {
throw new Error('TODO')
}

const formData = await request.formData()
const submission = parseWithZod(formData, { schema })

if (submission.status !== 'success') {
return data(
{ result: submission.reply(), data: null },
{ status: submission.status === 'error' ? 400 : 200 },
)
}

const { expression } = submission.value
const translator = new Translator(process.env.DEEPL_KEY!)
const translation = await translator.translate(
expression,
sourceLanguage,
targetLanguages,
)

if (!translation) {
return data({ result: submission.reply(), data: null }, { status: 400 })
}

return data({
result: submission.reply({ resetForm: true }),
data: {
expression,
translation,
},
})
}

export default function Page() {
const data = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
const isPending = useIsPending()

const [form, fields] = useForm({
lastResult: actionData?.result,
constraint: getZodConstraint(schema),
onValidate: ({ formData }) => parseWithZod(formData, { schema }),
})

const allErrors = Object.values(form.allErrors).flat()

return (
<article className="prose p-4 dark:prose-invert">
<h2>Try it out</h2>
<nav>
<ul>
{mainNav.map(({ href, title }) => (
<li key={href}>
<Link to={href} prefetch="intent">
{title}
<div className="mx-auto flex min-h-0 w-full max-w-[85ch] flex-grow flex-col font-serif">
<ScrollArea className="flex-grow px-4">
{actionData?.data && (
<div className="flex flex-col gap-y-4 py-4">
<div className="flex flex-col items-end">
<Card>
<CardContent className="px-4 py-2">
{actionData.data.expression}
</CardContent>
</Card>
</div>

<div className="flex flex-col items-start">
<Card className="min-w-[70%]">
<CardContent className="px-4 py-2">
<article className="flex flex-col gap-y-4">
{actionData.data.translation.map(
({ language, expressions }) => (
<div key={language}>
<h2>{targetLangConfig[language].label}</h2>
{expressions.map((expr) => (
<p key={expr}>{expr}</p>
))}
</div>
),
)}
</article>
</CardContent>
</Card>
</div>
</div>
)}
</ScrollArea>

<div className="relative px-4">
{!data.valid && (
<Alert
variant="destructive"
className="absolute inset-x-4 bottom-full mb-2 w-auto bg-background"
>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Missing Settings</AlertTitle>
<AlertDescription>
You're missing something in your{' '}
<Link
className="text-foreground underline"
to="/settings"
prefetch="intent"
>
settings
</Link>
</li>
))}
</ul>
</nav>
</article>
!
</AlertDescription>
</Alert>
)}
{allErrors.length > 0 && (
<Alert
variant="destructive"
className="absolute inset-x-4 bottom-full mb-2 w-auto bg-background"
>
<AlertCircle className="h-4 w-4" />
<AlertTitle>There is something wrong</AlertTitle>
<AlertDescription>{allErrors[0]}</AlertDescription>
</Alert>
)}
<Form
method="post"
className="flex flex-col gap-y-4 pb-4"
{...getFormProps(form)}
>
<div className="flex min-h-[60px] w-full flex-col gap-y-2 rounded-md border border-input bg-transparent p-3 shadow">
<textarea
{...getTextareaProps(fields.expression)}
placeholder="What do you want to express today?"
aria-label="Expression to be translated"
className="resize-none focus-visible:outline-none"
onInput={(e) => {
const target = e.target as HTMLTextAreaElement
target.style.height = 'auto'
target.style.height = target.scrollHeight + 'px'
}}
/>
<div className="flex flex-wrap justify-between gap-2">
<div>
<Button type="button" variant="ghost" size="icon" asChild>
<Link to="/settings">
<Settings2 />
</Link>
</Button>
</div>

<Button
size="icon"
type="submit"
className="ml-auto shrink-0 rounded-full"
disabled={isPending || !data.valid || !fields.expression.value}
>
{isPending ? (
<LoaderCircle className="animate-spin" />
) : (
<ArrowUp />
)}
</Button>
</div>
</div>
</Form>
</div>
</div>
)
}
3 changes: 0 additions & 3 deletions app/routes/elaborate.tsx

This file was deleted.

12 changes: 3 additions & 9 deletions app/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,11 @@ import {
type LoaderFunctionArgs,
type ActionFunctionArgs,
data,
HeadersFunction,
redirect,
} from '@remix-run/node'
import {
Form,
useActionData,
useFetcher,
useLoaderData,
} from '@remix-run/react'
import { Form, useActionData, useLoaderData } from '@remix-run/react'
import { Check, ChevronsUpDown } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { z } from 'zod'
import { ErrorList } from '#app/components/form.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand Down Expand Up @@ -81,7 +75,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
settingsSession.set('sourceLanguage', sourceLanguage)
settingsSession.set('targetLanguages', targetLanguages)

return redirect('/translate', {
return redirect('/', {
headers: {
'Set-Cookie': await settingsSessionStorage.commitSession(settingsSession),
},
Expand Down
Loading

0 comments on commit 0a7c531

Please sign in to comment.