mirror of
https://github.com/withastro/astro.git
synced 2025-01-06 22:10:10 -05:00
feat: add ticketing example
This commit is contained in:
parent
283d8d0bd0
commit
bed0b68a39
14 changed files with 5775 additions and 5 deletions
|
@ -3,13 +3,15 @@ import { vitePluginDb } from './vite-plugin-db.js';
|
|||
import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js';
|
||||
import { typegen } from './typegen.js';
|
||||
import { existsSync } from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
import { mkdir, rm, writeFile } from 'fs/promises';
|
||||
import { getLocalDbUrl } from './consts.js';
|
||||
import { createDb, setupDbTables } from './internal.js';
|
||||
import { astroConfigWithDbSchema } from './config.js';
|
||||
import { getAstroStudioEnv, type VitePlugin } from './utils.js';
|
||||
import { appTokenError } from './errors.js';
|
||||
import { errorMap } from './error-map.js';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export function integration(): AstroIntegration {
|
||||
return {
|
||||
|
@ -33,15 +35,18 @@ export function integration(): AstroIntegration {
|
|||
}
|
||||
dbPlugin = vitePluginDb({ connectToStudio: true, collections, appToken });
|
||||
} else {
|
||||
const dbUrl = getLocalDbUrl(config.root).href;
|
||||
const dbUrl = getLocalDbUrl(config.root);
|
||||
if (existsSync(dbUrl)) {
|
||||
await rm(dbUrl);
|
||||
}
|
||||
const db = await createDb({ collections, dbUrl, seeding: true });
|
||||
await mkdir(dirname(fileURLToPath(dbUrl)), { recursive: true });
|
||||
await writeFile(dbUrl, '');
|
||||
|
||||
const db = await createDb({ collections, dbUrl: dbUrl.href, seeding: true });
|
||||
await setupDbTables({ db, collections, logger });
|
||||
logger.info('Collections set up 🚀');
|
||||
|
||||
dbPlugin = vitePluginDb({ connectToStudio: false, collections, dbUrl });
|
||||
dbPlugin = vitePluginDb({ connectToStudio: false, collections, dbUrl: dbUrl.href });
|
||||
}
|
||||
|
||||
updateConfig({
|
||||
|
|
|
@ -44,6 +44,6 @@ export default defineConfig({
|
|||
integrations: [db()],
|
||||
db: {
|
||||
collections: { Author, Themes },
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
24
packages/db/test/fixtures/ticketing-example/.gitignore
vendored
Normal file
24
packages/db/test/fixtures/ticketing-example/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# Cloudflare
|
||||
.wrangler/
|
54
packages/db/test/fixtures/ticketing-example/README.md
vendored
Normal file
54
packages/db/test/fixtures/ticketing-example/README.md
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Astro Starter Kit: Basics
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ └── Card.astro
|
||||
│ ├── layouts/
|
||||
│ │ └── Layout.astro
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
57
packages/db/test/fixtures/ticketing-example/astro.config.ts
vendored
Normal file
57
packages/db/test/fixtures/ticketing-example/astro.config.ts
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import preact from "@astrojs/preact";
|
||||
import simpleStackForm from "simple-stack-form";
|
||||
import db, {
|
||||
defineCollection,
|
||||
defineWritableCollection,
|
||||
field,
|
||||
} from "@astrojs/db";
|
||||
import node from "@astrojs/node";
|
||||
|
||||
const Event = defineCollection({
|
||||
fields: {
|
||||
name: field.text(),
|
||||
description: field.text(),
|
||||
ticketPrice: field.number(),
|
||||
date: field.date(),
|
||||
location: field.text(),
|
||||
},
|
||||
data() {
|
||||
return [
|
||||
{
|
||||
name: "Sampha LIVE in Brooklyn",
|
||||
description:
|
||||
"Sampha is on tour with his new, flawless album Lahai. Come see the live performance outdoors in Prospect Park. Yes, there will be a grand piano 🎹",
|
||||
date: new Date("2024-01-01"),
|
||||
ticketPrice: 10000,
|
||||
location: "Brooklyn, NY",
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
const Ticket = defineWritableCollection({
|
||||
fields: {
|
||||
eventId: field.text(),
|
||||
email: field.text(),
|
||||
quantity: field.number(),
|
||||
newsletter: field.boolean({
|
||||
default: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [preact(), simpleStackForm(), db()],
|
||||
output: "server",
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
db: {
|
||||
studio: true,
|
||||
collections: {
|
||||
Event,
|
||||
Ticket,
|
||||
},
|
||||
},
|
||||
});
|
24
packages/db/test/fixtures/ticketing-example/package.json
vendored
Normal file
24
packages/db/test/fixtures/ticketing-example/package.json
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "eventbrite-from-scratch",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "pnpm astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.3.1",
|
||||
"@astrojs/db": "workspace:*",
|
||||
"@astrojs/node": "^8.0.0",
|
||||
"@astrojs/preact": "^3.0.1",
|
||||
"astro": "workspace:*",
|
||||
"open-props": "^1.6.17",
|
||||
"preact": "^10.6.5",
|
||||
"simple-stack-form": "^0.1.10",
|
||||
"typescript": "^5.3.2",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
5266
packages/db/test/fixtures/ticketing-example/pnpm-lock.yaml
vendored
Normal file
5266
packages/db/test/fixtures/ticketing-example/pnpm-lock.yaml
vendored
Normal file
File diff suppressed because it is too large
Load diff
9
packages/db/test/fixtures/ticketing-example/public/favicon.svg
vendored
Normal file
9
packages/db/test/fixtures/ticketing-example/public/favicon.svg
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 749 B |
131
packages/db/test/fixtures/ticketing-example/src/components/Form.tsx
vendored
Normal file
131
packages/db/test/fixtures/ticketing-example/src/components/Form.tsx
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Generated by simple:form
|
||||
|
||||
import { type ComponentProps, createContext } from "preact";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
import { navigate } from "astro:transitions/client";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FormState,
|
||||
type FormValidator,
|
||||
formNameInputProps,
|
||||
getInitialFormState,
|
||||
toSetValidationErrors,
|
||||
toTrackAstroSubmitStatus,
|
||||
toValidateField,
|
||||
validateForm,
|
||||
} from "simple:form";
|
||||
|
||||
export function useCreateFormContext(
|
||||
validator: FormValidator,
|
||||
fieldErrors?: FieldErrors
|
||||
) {
|
||||
const initial = getInitialFormState({ validator, fieldErrors });
|
||||
const [formState, setFormState] = useState<FormState>(initial);
|
||||
return {
|
||||
value: formState,
|
||||
set: setFormState,
|
||||
setValidationErrors: toSetValidationErrors(setFormState),
|
||||
validateField: toValidateField(setFormState),
|
||||
trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState),
|
||||
};
|
||||
}
|
||||
|
||||
export function useFormContext() {
|
||||
const formContext = useContext(FormContext);
|
||||
if (!formContext) {
|
||||
throw new Error(
|
||||
"Form context not found. `useFormContext()` should only be called from children of a <Form> component."
|
||||
);
|
||||
}
|
||||
return formContext;
|
||||
}
|
||||
|
||||
type FormContextType = ReturnType<typeof useCreateFormContext>;
|
||||
|
||||
const FormContext = createContext<FormContextType | undefined>(undefined);
|
||||
|
||||
export function Form({
|
||||
children,
|
||||
validator,
|
||||
context,
|
||||
fieldErrors,
|
||||
name,
|
||||
...formProps
|
||||
}: {
|
||||
validator: FormValidator;
|
||||
context?: FormContextType;
|
||||
fieldErrors?: FieldErrors;
|
||||
} & Omit<ComponentProps<"form">, "method" | "onSubmit">) {
|
||||
const formContext = context ?? useCreateFormContext(validator, fieldErrors);
|
||||
|
||||
return (
|
||||
<FormContext.Provider value={formContext}>
|
||||
<form
|
||||
{...formProps}
|
||||
method="POST"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
formContext.set((formState) => ({
|
||||
...formState,
|
||||
isSubmitPending: true,
|
||||
submitStatus: "validating",
|
||||
}));
|
||||
const parsed = await validateForm({ formData, validator });
|
||||
if (parsed.data) {
|
||||
const action =
|
||||
typeof formProps.action === "string"
|
||||
? formProps.action
|
||||
: // Check for Preact signals
|
||||
formProps.action?.value ?? "";
|
||||
navigate(action, { formData });
|
||||
return formContext.trackAstroSubmitStatus();
|
||||
}
|
||||
|
||||
formContext.setValidationErrors(parsed.fieldErrors);
|
||||
}}
|
||||
>
|
||||
{name ? <input {...formNameInputProps} value={name} /> : null}
|
||||
{children}
|
||||
</form>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function Input({
|
||||
onInput,
|
||||
...inputProps
|
||||
}: ComponentProps<"input"> & { name: string }) {
|
||||
const formContext = useFormContext();
|
||||
const fieldState = formContext.value.fields[inputProps.name];
|
||||
if (!fieldState) {
|
||||
throw new Error(
|
||||
`Input "${inputProps.name}" not found in form. Did you use the <Form> component?`
|
||||
);
|
||||
}
|
||||
|
||||
const { hasErroredOnce, validationErrors, validator } = fieldState;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
onBlur={async (e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (value === "") return;
|
||||
formContext.validateField(inputProps.name, value, validator);
|
||||
}}
|
||||
onInput={async (e) => {
|
||||
onInput?.(e);
|
||||
|
||||
if (!hasErroredOnce) return;
|
||||
const value = e.currentTarget.value;
|
||||
formContext.validateField(inputProps.name, value, validator);
|
||||
}}
|
||||
{...inputProps}
|
||||
/>
|
||||
{validationErrors?.map((e) => (
|
||||
<p key={e}>{e}</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
80
packages/db/test/fixtures/ticketing-example/src/layouts/Layout.astro
vendored
Normal file
80
packages/db/test/fixtures/ticketing-example/src/layouts/Layout.astro
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
import "open-props/normalize";
|
||||
import "open-props/style";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="Astro description" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<ViewTransitions handleForms />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
<style is:global>
|
||||
main {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: var(--size-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4);
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2);
|
||||
margin-bottom: var(--size-4);
|
||||
background: var(--surface-2);
|
||||
padding-inline: var(--size-4);
|
||||
padding-block: var(--size-6);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--red-6);
|
||||
margin-bottom: var(--size-2);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
form button {
|
||||
grid-column: 1 / -1;
|
||||
background: var(--orange-8);
|
||||
border-radius: var(--radius-2);
|
||||
padding-block: var(--size-2);
|
||||
}
|
||||
|
||||
.youre-going {
|
||||
background: var(--surface-2);
|
||||
padding: var(--size-2);
|
||||
border-radius: var(--radius-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-4);
|
||||
margin-bottom: var(--size-2);
|
||||
}
|
||||
|
||||
.newsletter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2);
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
40
packages/db/test/fixtures/ticketing-example/src/pages/[event]/_Ticket.tsx
vendored
Normal file
40
packages/db/test/fixtures/ticketing-example/src/pages/[event]/_Ticket.tsx
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { createForm } from "simple:form";
|
||||
import { Form, Input } from "../../components/Form";
|
||||
import { z } from "zod";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
export const ticketForm = createForm({
|
||||
email: z.string().email(),
|
||||
quantity: z.number().max(10),
|
||||
newsletter: z.boolean(),
|
||||
});
|
||||
|
||||
export function TicketForm({ price }: { price: number }) {
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
return (
|
||||
<>
|
||||
<Form validator={ticketForm.validator}>
|
||||
<h3>${(quantity * price) / 100}</h3>
|
||||
|
||||
<label for="quantity">Quantity</label>
|
||||
<Input
|
||||
id="quantity"
|
||||
{...ticketForm.inputProps.quantity}
|
||||
onInput={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
setQuantity(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label for="email">Email</label>
|
||||
<Input id="email" {...ticketForm.inputProps.email} />
|
||||
|
||||
<div class="newsletter">
|
||||
<Input id="newsletter" {...ticketForm.inputProps.newsletter} />
|
||||
<label for="newsletter">Hear about the next event in your area</label>
|
||||
</div>
|
||||
<button>Buy tickets</button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
56
packages/db/test/fixtures/ticketing-example/src/pages/[event]/index.astro
vendored
Normal file
56
packages/db/test/fixtures/ticketing-example/src/pages/[event]/index.astro
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import { Event, Ticket, db, eq } from "astro:db";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { TicketForm, ticketForm } from "./_Ticket";
|
||||
|
||||
if (!Astro.params.event) return Astro.redirect("/404");
|
||||
|
||||
const event = await db
|
||||
.select()
|
||||
.from(Event)
|
||||
.where(eq(Event.id, Astro.params.event))
|
||||
.get();
|
||||
|
||||
if (!event) return Astro.redirect("/404");
|
||||
|
||||
const res = await Astro.locals.form.getData(ticketForm);
|
||||
|
||||
if (res?.data) {
|
||||
await db.insert(Ticket).values({
|
||||
eventId: Astro.params.event,
|
||||
email: res.data.email,
|
||||
quantity: res.data.quantity,
|
||||
newsletter: res.data.newsletter,
|
||||
});
|
||||
}
|
||||
|
||||
const ticket = await db
|
||||
.select()
|
||||
.from(Ticket)
|
||||
.where(eq(Ticket.eventId, Astro.params.event))
|
||||
.get();
|
||||
---
|
||||
|
||||
<Layout title="Welcome to Astro.">
|
||||
<main>
|
||||
<h1>{event.name}</h1>
|
||||
<p>
|
||||
{event.description}
|
||||
</p>
|
||||
|
||||
<TicketForm price={event.ticketPrice} client:load />
|
||||
{
|
||||
ticket && (
|
||||
<section class="youre-going">
|
||||
<h2>You're going 🙌</h2>
|
||||
<p>
|
||||
You have purchased {ticket.quantity} tickets for {event.name}!
|
||||
</p>
|
||||
<p>
|
||||
Check <strong>{ticket.email}</strong> for your tickets.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</main>
|
||||
</Layout>
|
17
packages/db/test/fixtures/ticketing-example/src/pages/index.astro
vendored
Normal file
17
packages/db/test/fixtures/ticketing-example/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import { Event, db } from "astro:db";
|
||||
|
||||
const firstEvent = await db.select().from(Event).get();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Eventbrite</title>
|
||||
</head>
|
||||
<body>
|
||||
<meta http-equiv="refresh" content={`0; url=${firstEvent!.id}`} />
|
||||
</body>
|
||||
</html>
|
7
packages/db/test/fixtures/ticketing-example/tsconfig.json
vendored
Normal file
7
packages/db/test/fixtures/ticketing-example/tsconfig.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue