Skip to content

Commit 025c9ee

Browse files
committed
add example
1 parent 159fde8 commit 025c9ee

File tree

147 files changed

+3956
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+3956
-0
lines changed

examples/typed-sql/.env

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SESSION_SECRET="super-duper-secret"
2+
HONEYPOT_SECRET="very-secret"
3+
DATABASE_URL="file:./data.db"

examples/typed-sql/.eslintrc.cjs

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const vitestFiles = ['app/**/__tests__/**/*', 'app/**/*.{spec,test}.*']
2+
const testFiles = ['**/tests/**', ...vitestFiles]
3+
const appFiles = ['app/**']
4+
5+
/** @type {import('@types/eslint').Linter.BaseConfig} */
6+
module.exports = {
7+
extends: [
8+
'@remix-run/eslint-config',
9+
'@remix-run/eslint-config/node',
10+
'prettier',
11+
],
12+
rules: {
13+
// playwright requires destructuring in fixtures even if you don't use anything 🤷‍♂️
14+
'no-empty-pattern': 'off',
15+
'@typescript-eslint/consistent-type-imports': [
16+
'warn',
17+
{
18+
prefer: 'type-imports',
19+
disallowTypeAnnotations: true,
20+
fixStyle: 'inline-type-imports',
21+
},
22+
],
23+
'import/no-duplicates': ['warn', { 'prefer-inline': true }],
24+
'import/consistent-type-specifier-style': ['warn', 'prefer-inline'],
25+
'import/order': [
26+
'warn',
27+
{
28+
alphabetize: { order: 'asc', caseInsensitive: true },
29+
groups: [
30+
'builtin',
31+
'external',
32+
'internal',
33+
'parent',
34+
'sibling',
35+
'index',
36+
],
37+
},
38+
],
39+
},
40+
overrides: [
41+
{
42+
files: appFiles,
43+
excludedFiles: testFiles,
44+
rules: {
45+
'no-restricted-imports': [
46+
'error',
47+
{
48+
patterns: [
49+
{
50+
group: testFiles,
51+
message: 'Do not import test files in app files',
52+
},
53+
],
54+
},
55+
],
56+
},
57+
},
58+
{
59+
extends: ['@remix-run/eslint-config/jest-testing-library'],
60+
files: vitestFiles,
61+
rules: {
62+
'testing-library/no-await-sync-events': 'off',
63+
'jest-dom/prefer-in-document': 'off',
64+
},
65+
// we're using vitest which has a very similar API to jest
66+
// (so the linting plugins work nicely), but it means we have to explicitly
67+
// set the jest version.
68+
settings: {
69+
jest: {
70+
version: 28,
71+
},
72+
},
73+
},
74+
],
75+
}

examples/typed-sql/.gitignore

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
node_modules
2+
.DS_store
3+
4+
/build
5+
/public/build
6+
/server-build
7+
# Normally you'd ignore this file, but for the workshop we'll commit it
8+
# .env
9+
tsconfig.tsbuildinfo
10+
*.ignored.*
11+
12+
# normally you don't commit your database, but we're doing it here for the workshop
13+
# /prisma/data.db
14+
/prisma/data.db-journal
15+
/tests/prisma
16+
17+
/test-results/
18+
/playwright-report/
19+
/playwright/.cache/
20+
/tests/fixtures/email/
21+
/coverage
22+
23+
/other/cache.db
24+
25+
# Easy way to create temporary files/folders that won't accidentally be added to git
26+
*.local.*
27+
28+
# generated files
29+
# Normally these are ignored, but we're committing it so they don't have to be
30+
# generated on the fly for the workshop
31+
# We're not changing these duing the workshop anyway
32+
# /app/components/ui/icon.svg
33+
# /app/components/ui/icon.tsx

examples/typed-sql/.prettierignore

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
node_modules
2+
3+
/build
4+
/public/build
5+
/server-build
6+
.env
7+
tsconfig.tsbuildinfo
8+
*.ignored.*
9+
10+
/test-results/
11+
/playwright-report/
12+
/playwright/.cache/
13+
/tests/fixtures/email/*.json
14+
/coverage
15+
16+
package-lock.json

examples/typed-sql/.prettierrc.cjs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module.exports = {
2+
arrowParens: 'avoid',
3+
bracketSameLine: false,
4+
bracketSpacing: true,
5+
embeddedLanguageFormatting: 'auto',
6+
endOfLine: 'lf',
7+
htmlWhitespaceSensitivity: 'css',
8+
insertPragma: false,
9+
jsxSingleQuote: false,
10+
printWidth: 80,
11+
proseWrap: 'always',
12+
quoteProps: 'as-needed',
13+
requirePragma: false,
14+
semi: false,
15+
singleAttributePerLine: false,
16+
singleQuote: true,
17+
tabWidth: 2,
18+
trailingComma: 'all',
19+
useTabs: true,
20+
overrides: [
21+
{
22+
files: ['**/*.json'],
23+
options: {
24+
useTabs: false,
25+
},
26+
},
27+
{
28+
files: ['**/*.mdx'],
29+
options: {
30+
proseWrap: 'preserve',
31+
htmlWhitespaceSensitivity: 'ignore',
32+
},
33+
},
34+
],
35+
}

examples/typed-sql/README.mdx

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Typed SQL
2+
3+
This is an example of using Prisma Client's 5.19.0 `prisma.$queryRawTyped` for
4+
our search page instead of using zod for type safety.
+13
Loading
127 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
isRouteErrorResponse,
3+
useParams,
4+
useRouteError,
5+
} from '@remix-run/react'
6+
import { type ErrorResponse } from '@remix-run/router'
7+
import { getErrorMessage } from '#app/utils/misc.tsx'
8+
9+
type StatusHandler = (info: {
10+
error: ErrorResponse
11+
params: Record<string, string | undefined>
12+
}) => JSX.Element | null
13+
14+
export function GeneralErrorBoundary({
15+
defaultStatusHandler = ({ error }) => (
16+
<p>
17+
{error.status} {error.data}
18+
</p>
19+
),
20+
statusHandlers,
21+
unexpectedErrorHandler = error => <p>{getErrorMessage(error)}</p>,
22+
}: {
23+
defaultStatusHandler?: StatusHandler
24+
statusHandlers?: Record<number, StatusHandler>
25+
unexpectedErrorHandler?: (error: unknown) => JSX.Element | null
26+
}) {
27+
const error = useRouteError()
28+
const params = useParams()
29+
30+
if (typeof document !== 'undefined') {
31+
console.error(error)
32+
}
33+
34+
return (
35+
<div className="container mx-auto flex h-full w-full items-center justify-center bg-destructive p-20 text-h2 text-destructive-foreground">
36+
{isRouteErrorResponse(error)
37+
? (statusHandlers?.[error.status] ?? defaultStatusHandler)({
38+
error,
39+
params,
40+
})
41+
: unexpectedErrorHandler(error)}
42+
</div>
43+
)
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const floatingToolbarClassName =
2+
'absolute bottom-3 left-3 right-3 flex items-center gap-2 rounded-lg bg-muted/80 p-4 pl-5 shadow-xl shadow-accent backdrop-blur-sm md:gap-4 md:pl-7 justify-end'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React, { useId } from 'react'
2+
import { Input } from '#app/components/ui/input.tsx'
3+
import { Label } from '#app/components/ui/label.tsx'
4+
import { Textarea } from '#app/components/ui/textarea.tsx'
5+
6+
export type ListOfErrors = Array<string | null | undefined> | null | undefined
7+
8+
export function ErrorList({
9+
id,
10+
errors,
11+
}: {
12+
errors?: ListOfErrors
13+
id?: string
14+
}) {
15+
const errorsToRender = errors?.filter(Boolean)
16+
if (!errorsToRender?.length) return null
17+
return (
18+
<ul id={id} className="flex flex-col gap-1">
19+
{errorsToRender.map(e => (
20+
<li key={e} className="text-foreground-destructive text-[10px]">
21+
{e}
22+
</li>
23+
))}
24+
</ul>
25+
)
26+
}
27+
28+
export function Field({
29+
labelProps,
30+
inputProps,
31+
errors,
32+
className,
33+
}: {
34+
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
35+
inputProps: React.InputHTMLAttributes<HTMLInputElement>
36+
errors?: ListOfErrors
37+
className?: string
38+
}) {
39+
const fallbackId = useId()
40+
const id = inputProps.id ?? fallbackId
41+
const errorId = errors?.length ? `${id}-error` : undefined
42+
return (
43+
<div className={className}>
44+
<Label htmlFor={id} {...labelProps} />
45+
<Input
46+
id={id}
47+
aria-invalid={errorId ? true : undefined}
48+
aria-describedby={errorId}
49+
{...inputProps}
50+
/>
51+
<div className="min-h-[32px] px-4 pb-3 pt-1">
52+
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
53+
</div>
54+
</div>
55+
)
56+
}
57+
58+
export function TextareaField({
59+
labelProps,
60+
textareaProps,
61+
errors,
62+
className,
63+
}: {
64+
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
65+
textareaProps: React.TextareaHTMLAttributes<HTMLTextAreaElement>
66+
errors?: ListOfErrors
67+
className?: string
68+
}) {
69+
const fallbackId = useId()
70+
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
71+
const errorId = errors?.length ? `${id}-error` : undefined
72+
return (
73+
<div className={className}>
74+
<Label htmlFor={id} {...labelProps} />
75+
<Textarea
76+
id={id}
77+
aria-invalid={errorId ? true : undefined}
78+
aria-describedby={errorId}
79+
{...textareaProps}
80+
/>
81+
<div className="min-h-[32px] px-4 pb-3 pt-1">
82+
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
83+
</div>
84+
</div>
85+
)
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Form, useSearchParams, useSubmit } from '@remix-run/react'
2+
import { useId } from 'react'
3+
import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
4+
import { Icon } from './ui/icon.tsx'
5+
import { Input } from './ui/input.tsx'
6+
import { Label } from './ui/label.tsx'
7+
import { StatusButton } from './ui/status-button.tsx'
8+
9+
export function SearchBar({
10+
status,
11+
autoFocus = false,
12+
autoSubmit = false,
13+
}: {
14+
status: 'idle' | 'pending' | 'success' | 'error'
15+
autoFocus?: boolean
16+
autoSubmit?: boolean
17+
}) {
18+
const id = useId()
19+
const [searchParams] = useSearchParams()
20+
const submit = useSubmit()
21+
const isSubmitting = useIsPending({
22+
formMethod: 'GET',
23+
formAction: '/users',
24+
})
25+
26+
const handleFormChange = useDebounce((form: HTMLFormElement) => {
27+
submit(form)
28+
}, 400)
29+
30+
return (
31+
<Form
32+
method="GET"
33+
action="/users"
34+
className="flex flex-wrap items-center justify-center gap-2"
35+
onChange={e => autoSubmit && handleFormChange(e.currentTarget)}
36+
>
37+
<div className="flex-1">
38+
<Label htmlFor={id} className="sr-only">
39+
Search
40+
</Label>
41+
<Input
42+
type="search"
43+
name="search"
44+
id={id}
45+
defaultValue={searchParams.get('search') ?? ''}
46+
placeholder="Search"
47+
className="w-full"
48+
autoFocus={autoFocus}
49+
/>
50+
</div>
51+
<div>
52+
<StatusButton
53+
type="submit"
54+
status={isSubmitting ? 'pending' : status}
55+
className="flex w-full items-center justify-center"
56+
size="sm"
57+
>
58+
<Icon name="magnifying-glass" size="sm" />
59+
<span className="sr-only">Search</span>
60+
</StatusButton>
61+
</div>
62+
</Form>
63+
)
64+
}

0 commit comments

Comments
 (0)