├── .firebaserc
├── .gitignore
├── .vscode
├── extensions.json
└── launch.json
├── README.md
├── astro.config.mjs
├── firebase.json
├── package-lock.json
├── package.json
├── public
└── favicon.svg
├── src
├── components
│ ├── AudioPlayer.astro
│ ├── AudioRecorder.tsx
│ ├── Button.astro
│ ├── Container.astro
│ ├── Empty.astro
│ ├── H1.astro
│ ├── LinkCTA.astro
│ ├── Logo.astro
│ ├── RecordingPublishFailed.astro
│ ├── RecordingPublishSuccess.astro
│ └── UploadingRecording.astro
├── constants
│ ├── cookies.ts
│ └── firebase.ts
├── env.d.ts
├── layouts
│ └── BaseLayout.astro
├── pages
│ ├── api
│ │ ├── auth
│ │ │ └── signout.ts
│ │ └── recording.ts
│ ├── index.astro
│ ├── record.astro
│ ├── signin.astro
│ └── signup.astro
├── scripts
│ ├── authClientValidationRules.ts
│ ├── firebase
│ │ ├── init.ts
│ │ └── initServer.ts
│ └── simpleISOStringParser.ts
└── stores
│ └── audioRecording.ts
├── storage.rules
├── tailwind.config.cjs
└── tsconfig.json
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "be-audible"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 | # firebase logs
15 | firebase-debug.log
16 | ui-debug.log
17 |
18 |
19 | # environment variables
20 | .env
21 | .env.production
22 |
23 | # macOS-specific files
24 | .DS_Store
25 |
26 | # code editor files
27 | .vscode
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Be Audible!
2 |
3 | ## 👏🏽 A fullstack Astro application
4 |
5 | > This application is part of the [Understanding Astro](https://github.com/understanding-astro/understanding-astro-book) book.
6 |
7 |
8 |
9 | 
10 |
11 |
12 |
13 |
14 | All commands are run from the root of the project, from a terminal:
15 |
16 | | Command | Action |
17 | | :---------------- | :------------------------------------------- |
18 | | `npm install` | Installs dependencies |
19 | | `npm run dev` | Starts local dev server at `localhost:3000` |
20 | | `npm run build` | Build your production site to `./dist/` |
21 | | `npm run preview` | Preview your build locally, before deploying |
22 |
23 |
24 |
25 | ## 👀 Note
26 |
27 | - Complete application lives in the `master` branch i.e., `git checkout master`
28 | - Checkout to the `clean-slate` branch to code along or start afresh i.e., `git checkout clean-slate`
29 | - Building the application is covered in Chapter 8 of [the book](https://github.com/understanding-astro/understanding-astro-book)
30 |
--------------------------------------------------------------------------------
/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "astro/config";
2 |
3 | import tailwind from "@astrojs/tailwind";
4 | import react from "@astrojs/react";
5 |
6 | // https://astro.build/config
7 | export default defineConfig({
8 | output: "server",
9 | integrations: [tailwind(), react()],
10 | });
11 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "storage": {
3 | "rules": "storage.rules"
4 | },
5 | "emulators": {
6 | "auth": {
7 | "port": 9098
8 | },
9 | "storage": {
10 | "port": 3005
11 | },
12 | "ui": {
13 | "enabled": true,
14 | "port": 4001
15 | },
16 | "singleProjectMode": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "full-stack-astro",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview",
10 | "astro": "astro",
11 | "emulators": "firebase emulators:start"
12 | },
13 | "dependencies": {
14 | "@astrojs/react": "^2.2.0",
15 | "@astrojs/tailwind": "^3.1.3",
16 | "@nanostores/react": "^0.7.1",
17 | "@types/react": "^18.2.7",
18 | "@types/react-dom": "^18.2.4",
19 | "astro": "^2.5.5",
20 | "firebase": "^9.22.1",
21 | "firebase-admin": "^11.8.0",
22 | "nanoid": "^4.0.2",
23 | "nanostores": "^0.9.0",
24 | "react": "^18.2.0",
25 | "react-dom": "^18.2.0",
26 | "react-voice-recorder-player": "^1.1.0",
27 | "tailwindcss": "^3.3.2",
28 | "validator.tool": "^2.2.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/components/AudioPlayer.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { simpleISOStringParser } from "@scripts/simpleISOStringParser";
3 |
4 | type Props = {
5 | url: string;
6 | timeCreated: string;
7 | };
8 |
9 | const { url, timeCreated } = Astro.props;
10 | ---
11 |
12 |
15 |
16 |
17 |
}`)
18 |
19 |
20 |
21 | Listen:
22 |
23 | Audibled at {simpleISOStringParser(timeCreated)}
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/components/AudioRecorder.tsx:
--------------------------------------------------------------------------------
1 | import { VoiceRecorder } from "react-voice-recorder-player";
2 |
3 | type Props = {
4 | cta?: string;
5 | };
6 |
7 | export const Recorder = (props: Props) => {
8 | return (
9 |
10 | {
12 | // 👀 upload recording
13 | }}
14 | />
15 |
16 | {props.cta}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/Button.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | id?: string;
4 | class?: string;
5 | };
6 |
7 | const { id = "", class: classnames } = Astro.props;
8 | ---
9 |
10 |
19 |
--------------------------------------------------------------------------------
/src/components/Container.astro:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Empty.astro:
--------------------------------------------------------------------------------
1 |
2 |
🥺
3 |
This place is empty. No one's spoken — yet
4 |
5 |
--------------------------------------------------------------------------------
/src/components/H1.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/LinkCTA.astro:
--------------------------------------------------------------------------------
1 | ---
2 | type Props = {
3 | href: string;
4 | };
5 |
6 | const { href } = Astro.props;
7 | ---
8 |
9 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/Logo.astro:
--------------------------------------------------------------------------------
1 |
5 |
17 |
18 | BeAudible.
19 |
20 |
--------------------------------------------------------------------------------
/src/components/RecordingPublishFailed.astro:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/RecordingPublishSuccess.astro:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/UploadingRecording.astro:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/constants/cookies.ts:
--------------------------------------------------------------------------------
1 | export const TOKEN = "X-Token";
2 |
--------------------------------------------------------------------------------
/src/constants/firebase.ts:
--------------------------------------------------------------------------------
1 | export const BUCKET_NAME = "gs://be-audible.appspot.com";
2 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "react-voice-recorder-player";
4 |
5 | interface ImportMetaEnv {
6 | readonly FIREBASE_PRIVATE_KEY_ID: string;
7 | readonly FIREBASE_PRIVATE_KEY: string;
8 | readonly FIREBASE_PROJECT_ID: string;
9 | readonly FIREBASE_CLIENT_EMAIL: string;
10 | readonly FIREBASE_CLIENT_ID: string;
11 | readonly FIREBASE_AUTH_URI: string;
12 | readonly FIREBASE_TOKEN_URI: string;
13 | readonly FIREBASE_AUTH_PROVIDER_CERT_URL: string;
14 | readonly FIREBASE_CLIENT_CERT_URL: string;
15 | }
16 |
17 | interface ImportMeta {
18 | readonly env: ImportMetaEnv;
19 | }
20 |
--------------------------------------------------------------------------------
/src/layouts/BaseLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Button from "../components/Button.astro";
3 | import Logo from "../components/Logo.astro";
4 |
5 | type Props = {
6 | isPrivatePage?: boolean;
7 | };
8 |
9 | const { isPrivatePage = false } = Astro.props;
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Be Audible
21 |
22 |
23 |
24 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/pages/api/auth/signout.ts:
--------------------------------------------------------------------------------
1 | import type { APIRoute } from "astro";
2 |
3 | export const all: APIRoute = async (ctx) => {
4 | const method = ctx.request.method;
5 |
6 | return {
7 | body: JSON.stringify({
8 | method,
9 | message: "Unsupported HTTP method",
10 | }),
11 | status: 501,
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/pages/api/recording.ts:
--------------------------------------------------------------------------------
1 | import type { APIRoute } from "astro";
2 |
3 | export const all: APIRoute = async (ctx) => {
4 | const method = ctx.request.method;
5 |
6 | return {
7 | body: JSON.stringify({
8 | method,
9 | message: "Unsupported HTTP method",
10 | }),
11 | status: 501,
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import H1 from "@components/H1.astro";
3 | import Empty from "@components/Empty.astro";
4 | import BaseLayout from "@layouts/BaseLayout.astro";
5 | import Container from "@components/Container.astro";
6 | ---
7 |
8 |
9 |
10 | Hear what the world's saying
11 |
12 | Discover the voices of the world. Simply listen.
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/pages/record.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export const prerender = true;
3 |
4 | import H1 from "@components/H1.astro";
5 | import LinkCTA from "@components/LinkCTA.astro";
6 | import BaseLayout from "@layouts/BaseLayout.astro";
7 | import Container from "@components/Container.astro";
8 | import { Recorder } from "@components/AudioRecorder.jsx";
9 | ---
10 |
11 |
12 |
13 | Let your voice be heard
14 | Simply hit record.
15 |
16 |
17 | View recordings
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/pages/signin.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export const prerender = true;
3 |
4 | import BaseLayout from "@layouts/BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
12 |
15 |
16 |
19 | Sign in to your account
20 |
21 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/pages/signup.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export const prerender = true;
3 |
4 | import BaseLayout from "@layouts/BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
12 |
15 |
16 |
19 | Sign up: let your voice be heard
20 |
21 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/scripts/authClientValidationRules.ts:
--------------------------------------------------------------------------------
1 | // We could potentially bring in some TS schema validation e.g., via Zod https://github.com/colinhacks/zod
2 |
3 | export const authClientValidationRules = {
4 | email: {
5 | validate: (val: string = "") =>
6 | val.includes("@") ? "" : "Please check your email address format",
7 | },
8 | password: {
9 | validate: (val: string = "") =>
10 | val.length < 6 ? "Your password is too short" : "",
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/scripts/firebase/init.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Import the Firebase client modules
3 | */
4 | import { initializeApp } from "firebase/app";
5 | import { getAuth, connectAuthEmulator } from "firebase/auth";
6 |
7 | /**
8 | * The following represent the firebase client configuration for the application.
9 | * These are safe to be exposed on the client.
10 | * ⚠️ TODO: change these to match your project configuration.
11 | */
12 | const firebaseConfig = {
13 | projectId: "XXXXXXXXXXXXXXXXXXXX",
14 | messagingSenderId: "XXXXXXXXXXXXXXXXXXXX",
15 | authDomain: "XXXXXXXXXXXXXXXXXXXX",
16 | storageBucket: "XXXXXXXXXXXXXXXXXXXX",
17 | apiKey: "XXXXXXXXXXXXXXXXXXXX",
18 | appId: "XXXXXXXXXXXXXXXXXXXX",
19 | };
20 |
21 | // Initialize Firebase apps
22 | export const app = initializeApp(firebaseConfig);
23 | export const auth = getAuth(app);
24 |
25 | /**
26 | * For local testing add authentication emulator
27 | */
28 | if (import.meta.env.DEV) {
29 | connectAuthEmulator(auth, "http://localhost:9098");
30 | }
31 |
--------------------------------------------------------------------------------
/src/scripts/firebase/initServer.ts:
--------------------------------------------------------------------------------
1 | import type { ServiceAccount } from "firebase-admin";
2 | import { initializeApp, cert } from "firebase-admin/app";
3 |
4 | export const serviceAccount = {
5 | type: "service_account",
6 | universe_domain: "googleapis.com",
7 | project_id: import.meta.env.FIREBASE_PROJECT_ID,
8 | private_key_id: import.meta.env.FIREBASE_PRIVATE_KEY_ID,
9 | private_key: import.meta.env.FIREBASE_PRIVATE_KEY,
10 | client_email: import.meta.env.FIREBASE_CLIENT_EMAIL,
11 | client_id: import.meta.env.FIREBASE_CLIENT_ID,
12 | auth_uri: import.meta.env.FIREBASE_AUTH_URI,
13 | token_uri: import.meta.env.FIREBASE_TOKEN_URI,
14 | auth_provider_x509_cert_url: import.meta.env.FIREBASE_AUTH_PROVIDER_CERT_URL,
15 | client_x509_cert_url: import.meta.env.FIREBASE_CLIENT_CERT_URL,
16 | };
17 |
18 | /**
19 | * Firebase admin specifically checks for these
20 | * to use the emulators in development mode
21 | */
22 | if (import.meta.env.DEV) {
23 | process.env.FIREBASE_STORAGE_EMULATOR_HOST = "localhost:3005";
24 | process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9098";
25 | }
26 |
27 | let app: ReturnType;
28 |
29 | const getServerApp = () => {
30 | if (app || admin.apps.length) {
31 | return app;
32 | }
33 |
34 | app = initializeApp({
35 | credential: cert(serviceAccount as ServiceAccount),
36 | });
37 |
38 | return app;
39 | };
40 |
41 | export const serverApp = getServerApp();
42 |
--------------------------------------------------------------------------------
/src/scripts/simpleISOStringParser.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @param t ISO 8601 string
3 | * e.g., from '2023-05-28T09:11:18.376Z' to "11:11:18 AM"
4 | */
5 |
6 | export const simpleISOStringParser = (t: string) => {
7 | try {
8 | const dateTimeFormat = new Intl.DateTimeFormat([], {
9 | hour: "2-digit",
10 | minute: "2-digit",
11 | second: "2-digit",
12 | });
13 | return dateTimeFormat.format(new Date(t));
14 | } catch (error) {
15 | return t;
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/stores/audioRecording.ts:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/storage.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service firebase.storage {
3 | match /b/{bucket}/o {
4 | match /{allPaths=**} {
5 | allow read, write: if false;
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: "class",
4 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
5 | theme: {
6 | extend: {
7 | colors: {
8 | primary: {
9 | 50: "#eff6ff",
10 | 100: "#dbeafe",
11 | 200: "#bfdbfe",
12 | 300: "#93c5fd",
13 | 400: "#60a5fa",
14 | 500: "#3b82f6",
15 | 600: "#2563eb",
16 | 700: "#1d4ed8",
17 | 800: "#1e40af",
18 | 900: "#1e3a8a",
19 | },
20 | },
21 | },
22 | },
23 | plugins: [],
24 | };
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strictest",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react",
6 | "baseUrl": ".",
7 | "paths": {
8 | "@components/*": ["src/components/*"],
9 | "@layouts/*": ["src/layouts/*"],
10 | "@scripts/*": ["src/scripts/*"],
11 | "@stores/*": ["src/stores/*"],
12 | "@constants/*": ["src/constants/*"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------