Skip to content

Commit f7c3ba8

Browse files
atilafassinaMichaelDeBoeymachour
authored
feat: add xata example (#33)
* add example with Xata * Update xata/package.json Co-authored-by: Michaël De Boey <[email protected]> * Update xata/README.md Co-authored-by: Mehdi Achour <[email protected]> * Update xata/README.md Co-authored-by: Mehdi Achour <[email protected]> * Update xata/app/routes/index.tsx Co-authored-by: Michaël De Boey <[email protected]> * Update xata/package.json Co-authored-by: Michaël De Boey <[email protected]> * Update xata/README.md Co-authored-by: Mehdi Achour <[email protected]> * address change requests * Update `eslint` and `typescript` Co-authored-by: Michaël De Boey <[email protected]> * Update update types for react and react-dom Co-authored-by: Michaël De Boey <[email protected]> * Update xata/sandbox.config.json Co-authored-by: Mehdi Achour <[email protected]> * Update xata/.eslintrc.js Co-authored-by: Michaël De Boey <[email protected]> * Update xata/app/entry.client.tsx Co-authored-by: Michaël De Boey <[email protected]> * Update xata/app/entry.server.tsx Co-authored-by: Michaël De Boey <[email protected]> * Update xata/package.json Co-authored-by: Michaël De Boey <[email protected]> * Update xata/remix.env.d.ts Co-authored-by: Michaël De Boey <[email protected]> * address comments / update Xata cli and client * address comments / update Xata cli and client Co-authored-by: Michaël De Boey <[email protected]> Co-authored-by: Mehdi Achour <[email protected]>
1 parent bda2267 commit f7c3ba8

18 files changed

+641
-0
lines changed

xata/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4+
};

xata/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env

xata/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Xata example
2+
3+
This example showcases how to use Remix with [Xata](https://xata.io) as your data layer.
4+
5+
You get out-of-the-box:
6+
7+
- API Route to connect to your Xata database
8+
- Type-safe Codegen
9+
10+
## Preview
11+
12+
Open this example on [CodeSandbox](https://codesandbox.com):
13+
14+
- [![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/examples/tree/main/xata)
15+
16+
## Example
17+
18+
Execute [`create-remix`](https://github.com/remix-run/remix/tree/main/packages/create-remix) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
19+
20+
```sh
21+
npx create-remix@latest --template xata remix-xata-app
22+
```
23+
24+
### Link Your Xata Workspace and Run Codegen
25+
26+
```sh
27+
npm run xata:init
28+
```
29+
30+
In case you have a workspace already linked, you will need to use `--force` flag to push the template schema on top of an existing one.
31+
32+
> 💡 consider [installing the Xata CLI globally](https://xata.io/docs/cli/getting-started), it will likely improve your experience managing your databases
33+
34+
Once linked, you can just run `xata:codegen` to re-generate types.
35+
36+
### Start Coding
37+
38+
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
39+
40+
> 💡 the template will prompt you to create a dummy new table (`remix_with_xata_example`) with some useful resources.
41+
42+
## Related Links
43+
44+
- [Xata Docs](https://xata.io/docs)
45+
- [Xata VS Code Extension](https://marketplace.visualstudio.com/items?itemName=xata.xata) will make managing your data more comfortable
46+
- [Xata Discord](https://xata.io/discord)

xata/app/entry.client.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { RemixBrowser } from "@remix-run/react";
2+
import { startTransition, StrictMode } from "react";
3+
import { hydrateRoot } from "react-dom/client";
4+
5+
const hydrate = () =>
6+
startTransition(() =>
7+
hydrateRoot(
8+
document,
9+
<StrictMode>
10+
<RemixBrowser />
11+
</StrictMode>
12+
)
13+
);
14+
15+
if (window.requestIdleCallback) {
16+
window.requestIdleCallback(hydrate);
17+
} else {
18+
// Safari doesn't support requestIdleCallback
19+
// https://caniuse.com/requestidlecallback
20+
window.setTimeout(hydrate, 1);
21+
}

xata/app/entry.server.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { PassThrough } from "stream";
2+
3+
import type { EntryContext } from "@remix-run/node";
4+
import { Response } from "@remix-run/node";
5+
import { RemixServer } from "@remix-run/react";
6+
import isbot from "isbot";
7+
import { renderToPipeableStream } from "react-dom/server";
8+
9+
const ABORT_DELAY = 5000;
10+
11+
const handleRequest = (
12+
request: Request,
13+
responseStatusCode: number,
14+
responseHeaders: Headers,
15+
remixContext: EntryContext
16+
) =>
17+
isbot(request.headers.get("user-agent"))
18+
? handleBotRequest(
19+
request,
20+
responseStatusCode,
21+
responseHeaders,
22+
remixContext
23+
)
24+
: handleBrowserRequest(
25+
request,
26+
responseStatusCode,
27+
responseHeaders,
28+
remixContext
29+
);
30+
export default handleRequest;
31+
32+
const handleBotRequest = (
33+
request: Request,
34+
responseStatusCode: number,
35+
responseHeaders: Headers,
36+
remixContext: EntryContext
37+
) =>
38+
new Promise((resolve, reject) => {
39+
let didError = false;
40+
41+
const { pipe, abort } = renderToPipeableStream(
42+
<RemixServer context={remixContext} url={request.url} />,
43+
{
44+
onAllReady: () => {
45+
const body = new PassThrough();
46+
47+
responseHeaders.set("Content-Type", "text/html");
48+
49+
resolve(
50+
new Response(body, {
51+
headers: responseHeaders,
52+
status: didError ? 500 : responseStatusCode,
53+
})
54+
);
55+
56+
pipe(body);
57+
},
58+
onShellError: (error: unknown) => {
59+
reject(error);
60+
},
61+
onError: (error: unknown) => {
62+
didError = true;
63+
64+
console.error(error);
65+
},
66+
}
67+
);
68+
69+
setTimeout(abort, ABORT_DELAY);
70+
});
71+
72+
const handleBrowserRequest = (
73+
request: Request,
74+
responseStatusCode: number,
75+
responseHeaders: Headers,
76+
remixContext: EntryContext
77+
) =>
78+
new Promise((resolve, reject) => {
79+
let didError = false;
80+
81+
const { pipe, abort } = renderToPipeableStream(
82+
<RemixServer context={remixContext} url={request.url} />,
83+
{
84+
onShellReady: () => {
85+
const body = new PassThrough();
86+
87+
responseHeaders.set("Content-Type", "text/html");
88+
89+
resolve(
90+
new Response(body, {
91+
headers: responseHeaders,
92+
status: didError ? 500 : responseStatusCode,
93+
})
94+
);
95+
96+
pipe(body);
97+
},
98+
onShellError: (error: unknown) => {
99+
reject(error);
100+
},
101+
onError: (error: unknown) => {
102+
didError = true;
103+
104+
console.error(error);
105+
},
106+
}
107+
);
108+
109+
setTimeout(abort, ABORT_DELAY);
110+
});

xata/app/root.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { LinksFunction, MetaFunction } from '@remix-run/node'
2+
import {
3+
Links,
4+
LiveReload,
5+
Meta,
6+
Outlet,
7+
Scripts,
8+
ScrollRestoration,
9+
} from '@remix-run/react'
10+
11+
import styles from '~/styles.css'
12+
13+
export const links: LinksFunction = () => {
14+
return [{ rel: 'stylesheet', href: styles }]
15+
}
16+
17+
export const meta: MetaFunction = () => ({
18+
charset: 'utf-8',
19+
viewport: 'width=device-width,initial-scale=1',
20+
})
21+
22+
export default function App() {
23+
return (
24+
<html lang="en">
25+
<head>
26+
<Meta />
27+
<Links />
28+
</head>
29+
<body>
30+
<Outlet />
31+
<ScrollRestoration />
32+
<Scripts />
33+
<LiveReload />
34+
</body>
35+
</html>
36+
)
37+
}

xata/app/routes/index.tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { FC } from "react";
2+
import type { ActionArgs } from "@remix-run/node";
3+
import { json } from "@remix-run/node";
4+
import { Form, useFetcher, useLoaderData } from "@remix-run/react";
5+
6+
import type { RemixWithXataExampleRecord } from "~/lib/xata.codegen.server";
7+
import { getXataClient } from "~/lib/xata.codegen.server";
8+
9+
export const LINKS = [
10+
{
11+
description: "Everything you need to know about Xata APIs and tools.",
12+
title: "Xata Docs",
13+
url: "https://docs.xata.io",
14+
},
15+
{
16+
description: "In case you need to check some Remix specifics.",
17+
title: "Remix Docs",
18+
url: "https://remix.run/docs",
19+
},
20+
{
21+
description:
22+
"Maintain your flow by managing your Xata Workspace without ever leaving VS Code.",
23+
title: "Xata VS Code Extension",
24+
url: "https://marketplace.visualstudio.com/items?itemName=xata.xata",
25+
},
26+
{
27+
description: "Get help. Offer help. Show us what you built!",
28+
title: "Xata Discord",
29+
url: "https://xata.io/discord",
30+
},
31+
];
32+
33+
type TaskComponent = FC<
34+
Pick<RemixWithXataExampleRecord, "id" | "title" | "url" | "description">
35+
>;
36+
export const loader = async () => {
37+
const xata = getXataClient();
38+
const links = await xata.db.remix_with_xata_example.getAll();
39+
40+
return json(links);
41+
};
42+
43+
export const action = async ({ request }: ActionArgs) => {
44+
const xata = getXataClient();
45+
const { intent, id } = Object.fromEntries(await request.formData());
46+
47+
if (intent === "delete" && typeof id === "string") {
48+
await xata.db.remix_with_xata_example.delete(id);
49+
50+
return json({
51+
message: "delete: success",
52+
data: null,
53+
});
54+
}
55+
56+
if (intent === "create") {
57+
const newItem = await xata.db.remix_with_xata_example.create(LINKS);
58+
59+
return json({
60+
message: "create: success",
61+
data: newItem,
62+
});
63+
}
64+
65+
return json({
66+
message: "no action performed",
67+
data: null,
68+
});
69+
};
70+
71+
const Task: TaskComponent = ({ id, title, url, description }) => {
72+
const fetcher = useFetcher();
73+
74+
return fetcher.submission ? null : (
75+
<li key={url}>
76+
<a href={url ?? ""} rel="noopener noreferrer" target="_blank">
77+
{title}
78+
</a>
79+
<p>{description}</p>
80+
<fetcher.Form method="post">
81+
<input type="hidden" name="id" value={id} />
82+
<button type="submit" name="intent" value="delete">
83+
<span role="img" aria-label="delete item">
84+
🗑
85+
</span>
86+
</button>
87+
</fetcher.Form>
88+
</li>
89+
);
90+
};
91+
92+
export default function Index() {
93+
const links = useLoaderData<typeof loader>();
94+
95+
return (
96+
<main>
97+
<header>
98+
<img src="/flap.gif" alt="Xata Logo" />
99+
<h1>
100+
Remix with<span aria-hidden>&#8209;</span>xata
101+
</h1>
102+
</header>
103+
<article>
104+
{links.length > 0 ? (
105+
<ul>
106+
{links.map((link) => (
107+
<Task key={link.id} {...link} />
108+
))}
109+
</ul>
110+
) : (
111+
<section>
112+
<h2>No records found.</h2>
113+
<strong>
114+
Click the button below to add some useful links to your
115+
`remix_with_xata_example` table and see them here.
116+
</strong>
117+
<Form method="post">
118+
<button type="submit" name="intent" value="create">
119+
Push records to Xata
120+
</button>
121+
</Form>
122+
</section>
123+
)}
124+
</article>
125+
<footer>
126+
<span>
127+
Made by{" "}
128+
<a href="https://xata.io" rel="noopener noreferrer" target="_blank">
129+
<object data="/xatafly.svg" aria-label="Xata Logo" />
130+
</a>
131+
</span>
132+
</footer>
133+
</main>
134+
);
135+
}

0 commit comments

Comments
 (0)