Skip to content

Commit

Permalink
feat: add base setup, add seeding
Browse files Browse the repository at this point in the history
  • Loading branch information
nizamiza committed Mar 5, 2024
1 parent 046e788 commit 2e5766c
Show file tree
Hide file tree
Showing 42 changed files with 1,104 additions and 79 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
POCKET_BASE_URL="http://127.0.0.1:8090"

# For seeding database, use only locally...
POCKET_BASE_SEEDING_ADMIN_USER_EMAIL="[email protected]"
POCKET_BASE_SEEDING_ADMIN_USER_PASSWORD="<password>"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

# Fresh build directory
_fresh/

# npm dependencies
node_modules/

# PocketBase
pocketbase
pb_data
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# PocketBase
pb_*
!pb_migrations
shared/pb.d.ts
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,23 @@ mv ~/Downloads/pocketbase_0.20.7_darwin_arm64/pocketbase ./

## Getting Started

Create an `.env` file according to the `.env.example` at the root of the repository:

```sh
cp .env.example .env
```

Adjust the environment variables according to your setup.

Then, install the dependencies:

````sh

Start the PocketBase server:

```sh
deno task db:start
```
````
Then, you can start the application server:
Expand All @@ -78,6 +90,12 @@ That's it, you're ready to rock! 🎸

Deno has a great ecosystem of extensions for popular editors. You can learn more about them in the [official documentation](https://docs.deno.com/runtime/manual/getting_started/setup_your_environment#using-an-editoride).

## Deployment

You can deploy the app to Deno Deploy using the following command. Follow the instructions on [Fresh's official documentation](https://fresh.deno.dev/docs/getting-started/deploy-to-production) to learn more about the process.
To host a PocketBase instance, you can use [PocketHost](https://pockethost.io) service. Once you create an instance, make sure to add its URL to [environmental variables on Deno Deploy](https://docs.deno.com/deploy/manual/environment-variables).
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
Expand Down
19 changes: 19 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# To-do List For The Workshop

- [ ] [Install Deno](https://docs.deno.com/runtime/manual).
- [ ] [Create a Fresh project](https://fresh.deno.dev/docs/getting-started/create-a-project).
- [ ] [Download and copy PocketBase executable](https://pocketbase.io/docs/) into the project directory.
- [ ] Refer to the [README](./README.md) file for commands and additional info about the setup.
- [ ] Implement a simple note taking application:
- [ ] Create a login page with PocketBase's local auth provider.
- [ ] Implement logout functionality.
- [ ] Implement a global sidebar.
- [ ] Display user card with the user's name and avatar in the sidebar.
- [ ] Create a home page with a list of notes of the logged in user.
- [ ] Create a note edit page with a form to create or edit a note.
- [ ] Create a note detail page to view a note.
- [ ] Implement note deleting on the note detail page.
- [ ] Create a PocketBase instance on [PocketHost](https://pockethost.io).
- [ ] Create an account on [Deno Deploy](https://deno.com/deploy) and create a new project.
- [ ] Setup the environment variables on Deno Deploy.
- [ ] Deploy the app to Deno Deploy using `deployctl`.
10 changes: 9 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"manifest": "deno task cli manifest $(pwd)",
"start": "deno run -A --watch=static/,routes/ dev.ts",
"db:start": "./pocketbase serve",
"db:typegen": "npx [email protected] --db ./pb_data/data.db --out ./shared/pb.d.ts",
"db:seed": "deno run -A ./pb_seeds/seed.ts",
"db:clear": "deno run -A ./pb_seeds/seed.ts --clear",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
Expand All @@ -17,6 +20,7 @@
},
"exclude": ["**/_fresh/*"],
"imports": {
"$/": "./",
"$fresh/": "https://deno.land/x/[email protected]/",
"preact": "https://esm.sh/[email protected]",
"preact/": "https://esm.sh/[email protected]/",
Expand All @@ -25,7 +29,11 @@
"tailwindcss": "npm:[email protected]",
"tailwindcss/": "npm:/[email protected]/",
"tailwindcss/plugin": "npm:/[email protected]/plugin.js",
"$std/": "https://deno.land/[email protected]/"
"$std/": "https://deno.land/[email protected]/",
"pocketbase": "npm:pocketbase",
"zod": "https://deno.land/x/[email protected]/index.ts",
"zodenv": "https://deno.land/x/[email protected]/mod.ts",
"@faker-js/faker": "npm:@faker-js/[email protected]"
},
"compilerOptions": {
"jsx": "react-jsx",
Expand Down
12 changes: 12 additions & 0 deletions env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { load } from "$std/dotenv/mod.ts";
import { parse } from "zodenv";

await load({
export: true,
});

export const [config, env] = parse((e) => ({
POCKET_BASE_URL: e.url(),
POCKET_BASE_SEEDING_ADMIN_USER_EMAIL: e.email(),
POCKET_BASE_SEEDING_ADMIN_USER_PASSWORD: e.string(),
}));
18 changes: 12 additions & 6 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@

import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $api_joke from "./routes/api/joke.ts";
import * as $greet_name_ from "./routes/greet/[name].tsx";
import * as $_middleware from "./routes/_middleware.ts";
import * as $index from "./routes/index.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $login from "./routes/login.tsx";
import * as $FormField from "./islands/FormField.tsx";
import * as $Logout from "./islands/Logout.tsx";
import * as $StatusMessage from "./islands/StatusMessage.tsx";
import * as $UserCard from "./islands/UserCard.tsx";
import { type Manifest } from "$fresh/server.ts";

const manifest = {
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/api/joke.ts": $api_joke,
"./routes/greet/[name].tsx": $greet_name_,
"./routes/_middleware.ts": $_middleware,
"./routes/index.tsx": $index,
"./routes/login.tsx": $login,
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/FormField.tsx": $FormField,
"./islands/Logout.tsx": $Logout,
"./islands/StatusMessage.tsx": $StatusMessage,
"./islands/UserCard.tsx": $UserCard,
},
baseUrl: import.meta.url,
} satisfies Manifest;
Expand Down
16 changes: 0 additions & 16 deletions islands/Counter.tsx

This file was deleted.

84 changes: 84 additions & 0 deletions islands/FormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useSignal } from "@preact/signals";
import { JSX } from "preact";
import { oneLine } from "$/shared/utils.ts";

type InputElement = {
input: JSX.HTMLAttributes<HTMLInputElement>;
textarea: JSX.HTMLAttributes<HTMLTextAreaElement>;
};

export type FormFieldProps<T extends keyof InputElement> =
& Omit<
InputElement[T],
"name"
>
& {
element?: T;
label: string;
name: string;
};

export default function FormField<T extends keyof InputElement>({
element = "input" as T,
label,
...props
}: FormFieldProps<T>) {
const showPass = useSignal(false);

return (
<fieldset
class={oneLine`
flex flex-col gap-2 min-w-[15rem] p-2 rounded-lg relative isolate
border-[1.5px] border-[--surface-passive] focus-within:border-[--accent]
`}
>
<legend
class={oneLine`
text-sm px-2 py-0.5 rounded-md w-fit
${props.required ? "after:content-['*'] after:text-[--error]" : ""}
`}
>
{label}
</legend>
{element === "input"
? (
<input
aria-label={label}
class={oneLine`
py-2 px-4 rounded-md bg-[--surface] accent-focus z-[1]
`}
{...(props as InputElement["input"])}
/>
)
: (
<textarea
aria-label={label}
class={oneLine`
py-2 px-4 rounded-md bg-[--surface] accent-focus
`}
rows={7}
{...(props as InputElement["textarea"])}
>
</textarea>
)}
{props.type === "password" && (
<button
class="text-xs absolute top-3.5 right-4 z-[2]"
type="button"
onClick={() => {
const input = document.querySelector<HTMLInputElement>(
`input[name=${props.name}]`,
);

if (input) {
input.type = input.type === "password" ? "text" : "password";
showPass.value = !showPass.value;
}
}}
>
{showPass.value ? "Hide" : "Show"}
</button>
)}
</fieldset>
);
}
38 changes: 38 additions & 0 deletions islands/Logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { JSX } from "preact";
import { useRef } from "preact/hooks";
import { oneLine } from "$/shared/utils.ts";

export type LogoutProps = JSX.HTMLAttributes<HTMLButtonElement>;

export default function Logout({
class: className,
children,
...props
}: LogoutProps) {
const dialogRef = useRef<HTMLDialogElement>(null);

return (
<>
<button
class={oneLine`text-[--error] ${className}`}
onClick={() => dialogRef.current?.showModal()}
{...props}
>
{children || "Logout"}
</button>
<dialog ref={dialogRef}>
<header>
<h1 class="h2">Are you sure you want to logout?</h1>
</header>
<footer>
<form method="dialog">
<button type="submit">Cancel</button>
</form>
<a href="/logout">
<button tabIndex={-1}>Logout</button>
</a>
</footer>
</dialog>
</>
);
}
73 changes: 73 additions & 0 deletions islands/StatusMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Signal } from "@preact/signals";
import { JSX } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { oneLine } from "$/shared/utils.ts";

export type StatusMessageProps =
& Omit<
JSX.HTMLAttributes<HTMLDialogElement>,
"open"
>
& {
open?: boolean | Signal<boolean>;
type: "success" | "error" | "info";
};

export default function StatusMessage({
type,
children,
open,
...props
}: StatusMessageProps) {
const dialogRef = useRef<HTMLDialogElement>(null);

useEffect(() => {
const isBooleanOpen = typeof open === "boolean" && open;
const isSignalOpen = !isBooleanOpen && open && "value" in open &&
open.value;

if (isBooleanOpen || isSignalOpen) {
dialogRef.current?.show();
} else {
dialogRef.current?.close();
}
}, [open]);

return (
<dialog
ref={dialogRef}
tabIndex={-1}
class={oneLine`
status fixed top-auto right-4 bottom-4 left-auto ml-4
translate-x-0 translate-y-0 m-0 z-[999] overflow-x-scroll
backdrop-blur-xl
open:flex flex-row gap-4 items-center px-4 py-3
rounded-lg bg-[--surface-passive]
${
type === "success"
? "text-[--success]"
: type === "error"
? "text-[--error]"
: "text-[--info]"
}
${
type === "success"
? "[--accent-color:--success]"
: type === "error"
? "[--accent-color:--error]"
: "[--accent-color:--info]"
}
`}
{...props}
>
<span class="text-left flex-grow first-letter:capitalize">
{children}
</span>
<form method="dialog">
<button class="text-xs" type="submit">
Close
</button>
</form>
</dialog>
);
}
Loading

1 comment on commit 2e5766c

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on 2e5766c Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

UNCAUGHT_EXCEPTION

MissingEnvVarsError: The following variables were defined in the example file but are not present in the environment:
  POCKET_BASE_URL, POCKET_BASE_SEEDING_ADMIN_USER_EMAIL, POCKET_BASE_SEEDING_ADMIN_USER_PASSWORD

Make sure to add them to your env file.

If you expect any of these variables to be empty, you can set the allowEmptyValues option to true.
    at assertSafe (https://deno.land/[email protected]/dotenv/mod.ts:356:11)
    at loadSync (https://deno.land/[email protected]/dotenv/mod.ts:253:5)
    at https://deno.land/[email protected]/dotenv/load.ts:11:3

Please sign in to comment.