0
Fork 0
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:
bholmesdev 2024-01-18 17:32:02 -05:00 committed by Nate Moore
parent 283d8d0bd0
commit bed0b68a39
14 changed files with 5775 additions and 5 deletions

View file

@ -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({

View file

@ -44,6 +44,6 @@ export default defineConfig({
integrations: [db()],
db: {
collections: { Author, Themes },
}
},
});

View 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/

View 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).

View 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,
},
},
});

View 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"
}
}

File diff suppressed because it is too large Load diff

View 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

View 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>
))}
</>
);
}

View 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>

View 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>
</>
);
}

View 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>

View 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>

View file

@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}