├── LICENSE ├── README.md ├── gatsby ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── backend │ ├── .vscode │ │ └── settings.json │ ├── Chisel.toml │ ├── endpoints │ │ └── comments.ts │ ├── models │ │ └── BlogComment.ts │ ├── package-lock.json │ ├── package.json │ ├── policies │ │ └── pol.yml │ └── tsconfig.json ├── content │ └── blog │ │ ├── hello-world │ │ └── index.md │ │ ├── my-second-post │ │ └── index.md │ │ └── new-beginnings │ │ └── index.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── package-lock.json ├── package.json ├── plugins │ └── gatsby-chisel │ │ ├── gatsby-node.js │ │ └── package.json ├── postcss.config.js ├── src │ ├── components │ │ ├── comment-form.js │ │ ├── comment-section.js │ │ ├── comment.js │ │ ├── layout.js │ │ ├── loader.js │ │ └── seo.js │ ├── images │ │ └── gatsby-icon.png │ ├── pages │ │ ├── 404.js │ │ ├── index.js │ │ └── using-typescript.tsx │ ├── services │ │ └── api.js │ ├── styles │ │ ├── global.css │ │ ├── normalize.css │ │ └── tailwind.css │ └── templates │ │ └── blog-post.js ├── static │ ├── favicon.ico │ └── robots.txt └── tailwind.config.js ├── nextjs ├── .env.local.sample ├── .gitignore ├── Chisel.toml ├── README.md ├── endpoints │ ├── get_all_people.js │ └── import_person.js ├── lib │ └── withSession.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.js │ ├── api │ │ ├── hello.js │ │ └── logout.js │ ├── index.js │ └── profile.js ├── policies │ └── policies.yml ├── styles │ ├── Home.module.css │ └── globals.css └── types │ └── types.ts ├── remix ├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows │ │ └── deploy.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .npmrc ├── .prettierignore ├── Dockerfile ├── README.md ├── app │ ├── db.server.ts │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── models │ │ ├── note.server.ts │ │ ├── post.server.ts │ │ └── user.server.ts │ ├── root.tsx │ ├── routes │ │ ├── healthcheck.tsx │ │ ├── index.tsx │ │ ├── join.tsx │ │ ├── login.tsx │ │ ├── logout.tsx │ │ ├── notes.tsx │ │ ├── notes │ │ │ ├── $noteId.tsx │ │ │ ├── index.tsx │ │ │ └── new.tsx │ │ └── posts │ │ │ ├── $id.tsx │ │ │ ├── admin.tsx │ │ │ ├── admin │ │ │ ├── $id.tsx │ │ │ ├── index.tsx │ │ │ └── new.tsx │ │ │ └── index.tsx │ ├── session.server.ts │ ├── utils.test.ts │ └── utils.ts ├── chiselstrike │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── Chisel.toml │ ├── endpoints │ │ └── posts.ts │ ├── models │ │ └── Post.ts │ ├── package.json │ ├── policies │ │ └── .gitkeep │ └── tsconfig.json ├── cypress.config.ts ├── cypress │ ├── .eslintrc.js │ ├── e2e │ │ └── smoke.cy.ts │ ├── fixtures │ │ └── example.json │ ├── support │ │ ├── commands.ts │ │ ├── create-user.ts │ │ ├── delete-user.ts │ │ └── e2e.ts │ └── tsconfig.json ├── fly.toml ├── mocks │ ├── README.md │ └── index.js ├── package.json ├── prisma │ ├── migrations │ │ ├── 20220713162558_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── seed.js ├── public │ └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── remix.init │ ├── gitignore │ ├── index.js │ └── package.json ├── start.sh ├── tailwind.config.js ├── test │ └── setup-test-env.ts ├── tsconfig.json └── vitest.config.ts └── streaming ├── .gitignore ├── Chisel.toml ├── README.md ├── endpoints └── top.ts ├── events ├── .gitkeep └── book-updates.ts ├── models ├── .gitkeep └── TopOfBook.ts ├── package-lock.json ├── package.json ├── policies └── .gitkeep └── tsconfig.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ChiselStrike, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChiselStrike Examples 2 | 3 | Welcome to the repository of ChiselStrike examples! 4 | 5 | * [Blog application using Gatsby with ChiselStrike](./gatsby) 6 | * [Simple application using Next.js with ChiselStrike](./nextjs) 7 | * [Adding blog posts to the Remix tutorial with ChiselStrike](./remix) 8 | * [Todo application using Replicache and ChiselStrike](https://github.com/penberg/replicache-todo) 9 | * [URL shortener using Gatsby and ChiselStrike](https://github.com/apoorv-on-git/chisel-shortner) by @apoorv-on-git 10 | * [Streaming example using Kafka and ChiselStrike](./streaming) 11 | 12 | If you have an example application, please open a pull request to add it to this list! 13 | -------------------------------------------------------------------------------- /gatsby/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # ChiselStrike 72 | backend/chiseld-data.db-shm 73 | backend/chiseld-data.db 74 | backend/chiseld.db 75 | backend/chiseld-data.db-wal 76 | backend/chiseld.db-shm 77 | backend/chiseld.db-wal 78 | -------------------------------------------------------------------------------- /gatsby/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /gatsby/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /gatsby/README.md: -------------------------------------------------------------------------------- 1 | # ChiselStrike with Gatsby Example 2 | 3 | This example implements a blog page where there are multiple posts made in 4 | markdown and for each post anyone can comment and see other people's comments 5 | through ChiselStrike. 6 | 7 | ## Instructions to run the example 8 | 9 | First, we need to set up ChiselStrike (located at the `backend` directory) and 10 | Gatsby to run the project. We can do both at the same time by just installing 11 | the necessary packages at the root of the project: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | And then to start Gatsby and ChiselStrike in development mode, run the command: 18 | 19 | ```bash 20 | npm run start 21 | ``` 22 | 23 | This should start Gatsby and the ChiselStrike server at the same time. The blog 24 | should be accessible in `localhost:8000`, selecting a post and creating a 25 | comment should be possible, as well as seeing previous comments on each blog 26 | post. 27 | 28 | Chiselstrike is brought up together with Gatsby because of the 29 | `gatsby-chisel` plugin. If this plugin is not used, then ChiselStrike needs to 30 | be started manually on the `backend` directory with `npm run dev` in another terminal from 31 | the one with Gatsby. 32 | 33 | --- 34 | 35 | **NOTE** 36 | 37 | `npm install` at the root of the project is going to install Gatsby packages 38 | **and then** go into the `backend` directory and install ChiselStrike's 39 | packages. This happens automatically because of the `postinstall` script on the 40 | `package.json` that handles this. Without it, you would have to manually run 41 | `npm install` twice, once in the root for Gatsby and another in the `backend` 42 | for ChiselStrike 43 | 44 | --- 45 | 46 | ## How this example was made 47 | 48 | This section details the step-by-step process that was used to create this 49 | example. This serves more as a report of sorts, and it should be used for 50 | understanding the process to create this example; Most importantly, this section can 51 | later be used as a base for a future blog post for ChiselStrike with Gatsby. 52 | 53 | ### Step 0 - Gatsby Setup 54 | 55 | For the Gatsby setup, we used a 56 | [starter](https://www.gatsbyjs.com/starters/gatsbyjs/gatsby-starter-blog) on the 57 | Gatsby site that is a boilerplate for blog sites. Then added Tailwind CSS to 58 | make styling easier as well as getting a template for a comment section with 59 | this [example](https://tailwindcomponents.com/component/comment-form). 60 | 61 | The starter uses blogs written in Markdown as the source of content. While we 62 | could save these blogs on the database, we felt that we should respect the ways 63 | the Gatsby community deals with posts and just extend these Markdown posts with 64 | comments that come from ChiselStrike. This way, Gatsby users can see that they 65 | don't have to ditch their entire way of doing things up until now, ChiselStrike 66 | would just be added on top of what is possible. So on unto adding comments to 67 | these posts! 68 | 69 | ### Step 1 - ChiselStrike types 70 | 71 | Here were are assuming that you already have the ChiselStrike working and 72 | `chisel` is accessible with the path variables. 73 | 74 | The first thing we want to do is to create the model (also called 75 | types/entities) of the database. Because we only want `comments` to be dynamic 76 | for this project, we only have to create a `BlogComment` model representing a 77 | comment. We can do this by initiating ChiselStrike on a directory with 78 | `npx create-chiselstrike-app backend` and then create the model on the 79 | `backend/models` directory like so: 80 | 81 | ```typescript 82 | class BlogComment { 83 | postId: string // The Gatsby's id for the post 84 | content: string // The comment's text 85 | postedAt: string // When the post was posted in ISO string format 86 | } 87 | ``` 88 | 89 | In this case, we create a `postId` so that we can associate a `BlogComment` with 90 | the id that Gatsby assigns to a post through the markdown local API. As for 91 | `postedAt` being a string is due to the `Date` type not being supported by 92 | ChiselStrike at the moment, so it holds a string with the ISO format for the 93 | date, which is later going to be parsed by Javascript for date operations. 94 | 95 | ### Step 2 - ChiselStrike endpoints 96 | 97 | For this example, we are going to need one endpoint with two methods: one for 98 | the `GET` method that will either get all comments (for testing purposes) or all 99 | comments for a specified post, and one for the `POST` method that will create a 100 | new comment for a specific post. 101 | 102 | The endpoint for this is just like the one on `backend/endpoints/comments.ts`. 103 | Notice that for the `GET` method, if no `postId` is given as a query parameter, 104 | then the endpoint will return all comments of the database (this can be 105 | dangerous if no pagination is used), otherwise the comments fetched will 106 | be only those for a specific post with `postId`. 107 | 108 | For the `POST` method that will create a new post, it just calls the `save` 109 | method from `BlogComment` using the body given by the request, while also 110 | augmenting it by including a timestamp for the creation date. 111 | 112 | With these endpoints, we can now do the frontend code to implement comments on 113 | our blog! 114 | 115 | ### Step 3 - Calling ChiselStrike on the frontend 116 | 117 | First we need a `Comment` component that is going to be used to represent a 118 | comment. This can be found on `src/components/Comment`, with the whole comment 119 | section being found on `src/components/comment-sections.js`. 120 | 121 | Gatsby has many way to get data from sources and render a page, such as: static 122 | site generation (SSG), server side rendering (SSR) and client side rendering. 123 | Because comments are dynamic data, we can't use SSG, otherwise they would be 124 | become outdated pretty easily and we would have to build the project again to 125 | get the new comments every time. 126 | 127 | While SSR seems good on paper for this, comments are a non-essential part of the 128 | page in which the most essential part (the blog post) already was rendered in 129 | SSG (which is fast). If we were to put SSR, the SSG used for the blog post is 130 | going to be useless because the page request would now have to go to the server, 131 | the server would get the comments, it would build the page and then deliver the 132 | built page to the user, totally ignoring the fact that the component for the 133 | post was already built. SSR would make the delivery of the essential part of the 134 | page slow because it would deliver the post + comments at the same time. 135 | 136 | What if we had a way to deliver the SSG post as fast as possible **and then** do 137 | the request for comments and render the comments section when that's ready? That 138 | is what client side render does! So for this example, we are going to treat the 139 | component as a normal React component and get the data when the component 140 | javascript's loads up, as well as when a new comment is created by the user. 141 | 142 | To do this we extended the `src/templates/blog-post.js` that came with the 143 | starter, adding a comment section and logic to get and create comments through a 144 | Axios request to ChiselStrike. Some notable points are: 145 | 146 | - `getCommentsFromChisel` is the function used to get all comments from a post 147 | from ChiselStrike and format them. 148 | - `handleCommentCreation` is the function that is going to be used to create a new 149 | comment, as well as update the comments after that is done. 150 | - `useEffect` this is going to get the comments the first time when the blog 151 | post renders. This will be run only after Javascript is loaded into the page, 152 | so the post gets delivered first and then the comments are fetched. 153 | -------------------------------------------------------------------------------- /gatsby/backend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "chiselstrike" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /gatsby/backend/Chisel.toml: -------------------------------------------------------------------------------- 1 | models = ["models"] 2 | endpoints = ["endpoints"] 3 | policies = ["policies"] 4 | -------------------------------------------------------------------------------- /gatsby/backend/endpoints/comments.ts: -------------------------------------------------------------------------------- 1 | import { responseFromJson } from "@chiselstrike/api" 2 | import { BlogComment } from "../models/BlogComment" 3 | 4 | type Handler = (req: Request, res: Response) => Response | Promise 5 | 6 | const handlePost: Handler = async req => { 7 | const payload = await req.json() 8 | const created = BlogComment.build({ 9 | ...payload, 10 | postedAt: new Date().toISOString(), 11 | }) 12 | await created.save() 13 | return responseFromJson("inserted " + created.id) 14 | } 15 | 16 | const handleGet: Handler = async req => { 17 | const url = new URL(req.url) 18 | const postId = url.searchParams.get("postId") ?? undefined 19 | const comments = await BlogComment.findMany({ postId }) 20 | return responseFromJson(comments) 21 | } 22 | 23 | const handlers: Record = { 24 | POST: handlePost, 25 | GET: handleGet, 26 | } 27 | 28 | export default async function chisel(req: Request, res: Response) { 29 | if (handlers[req.method] === undefined) 30 | return new Response(`Unsupported method ${req.method}`, { status: 405 }) 31 | return handlers[req.method](req, res) 32 | } 33 | -------------------------------------------------------------------------------- /gatsby/backend/models/BlogComment.ts: -------------------------------------------------------------------------------- 1 | import { ChiselEntity } from "@chiselstrike/api" 2 | 3 | export class BlogComment extends ChiselEntity { 4 | postId: string = "" 5 | content: string = "" 6 | postedAt: string = "" 7 | } 8 | -------------------------------------------------------------------------------- /gatsby/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "dev": "chisel dev" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@chiselstrike/api": "latest", 13 | "typescript": "^4.5.4" 14 | }, 15 | "devDependencies": { 16 | "@chiselstrike/cli": "latest" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gatsby/backend/policies/pol.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiselstrike/chiselstrike-examples/78f7f0374155a3cdcc3a3db1502c69f00a2a8e02/gatsby/backend/policies/pol.yml -------------------------------------------------------------------------------- /gatsby/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESnext", 6 | "allowJs": true, 7 | "module": "commonjs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gatsby/content/blog/hello-world/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | date: "2015-05-01T22:12:03.284Z" 4 | description: "Hello World" 5 | --- 6 | 7 | This is my first post on my new fake blog! How exciting! 8 | 9 | I'm sure I'll write a lot more interesting things in the future. 10 | -------------------------------------------------------------------------------- /gatsby/content/blog/my-second-post/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: My Second Post! 3 | date: "2015-05-06T23:46:37.121Z" 4 | --- 5 | 6 | Wow! I love blogging so much already. 7 | 8 | Did you know that "despite its name, salted duck eggs can also be made from 9 | chicken eggs, though the taste and texture will be somewhat different, and the 10 | egg yolk will be less rich."? 11 | ([Wikipedia Link](https://en.wikipedia.org/wiki/Salted_duck_egg)) 12 | 13 | Yeah, I didn't either. 14 | -------------------------------------------------------------------------------- /gatsby/content/blog/new-beginnings/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: New Beginnings 3 | date: "2015-05-28T22:40:32.169Z" 4 | description: This is a custom description for SEO and Open Graph purposes, rather than the default generated excerpt. Simply add a description field to the frontmatter. 5 | --- 6 | 7 | Far far away, behind the word mountains, far from the countries Vokalia and 8 | Consonantia, there live the blind texts. Separated they live in Bookmarksgrove 9 | right at the coast of the Semantics, a large language ocean. A small river named 10 | Duden flows by their place and supplies it with the necessary regelialia. 11 | -------------------------------------------------------------------------------- /gatsby/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | // custom typefaces 2 | import "typeface-montserrat" 3 | import "typeface-merriweather" 4 | // normalize CSS across browsers 5 | import "./src/styles/normalize.css" 6 | // custom CSS styles 7 | import "./src/styles/global.css" 8 | import './src/styles/tailwind.css' 9 | 10 | // Highlighting for code blocks 11 | import "prismjs/themes/prism.css" 12 | -------------------------------------------------------------------------------- /gatsby/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const { createProxyMiddleware } = require("http-proxy-middleware") 2 | module.exports = { 3 | siteMetadata: { 4 | title: `Gatsby ChiselStrike Starter Blog`, 5 | author: { 6 | name: `ChiselStrike Beta Team`, 7 | summary: `Thanks for joining the ChiselStrike beta`, 8 | }, 9 | description: `A starter blog demonstrating what Gatsby can do with ChiselStrike.`, 10 | siteUrl: `https://www.chiselstrike.com`, 11 | }, 12 | plugins: [ 13 | "gatsby-plugin-postcss", 14 | `gatsby-plugin-image`, 15 | { 16 | resolve: `gatsby-chisel`, 17 | options: { 18 | path: `${__dirname}/backend`, 19 | }, 20 | }, 21 | { 22 | resolve: `gatsby-source-filesystem`, 23 | options: { 24 | path: `${__dirname}/content/blog`, 25 | name: `blog`, 26 | }, 27 | }, 28 | { 29 | resolve: `gatsby-source-filesystem`, 30 | options: { 31 | name: `images`, 32 | path: `${__dirname}/src/images`, 33 | }, 34 | }, 35 | { 36 | resolve: `gatsby-transformer-remark`, 37 | options: { 38 | plugins: [ 39 | { 40 | resolve: `gatsby-remark-images`, 41 | options: { 42 | maxWidth: 630, 43 | }, 44 | }, 45 | { 46 | resolve: `gatsby-remark-responsive-iframe`, 47 | options: { 48 | wrapperStyle: `margin-bottom: 1.0725rem`, 49 | }, 50 | }, 51 | `gatsby-remark-prismjs`, 52 | `gatsby-remark-copy-linked-files`, 53 | `gatsby-remark-smartypants`, 54 | ], 55 | }, 56 | }, 57 | `gatsby-transformer-sharp`, 58 | `gatsby-plugin-sharp`, 59 | // { 60 | // resolve: `gatsby-plugin-google-analytics`, 61 | // options: { 62 | // trackingId: `ADD YOUR TRACKING ID HERE`, 63 | // }, 64 | // }, 65 | { 66 | resolve: `gatsby-plugin-feed`, 67 | options: { 68 | query: ` 69 | { 70 | site { 71 | siteMetadata { 72 | title 73 | description 74 | siteUrl 75 | site_url: siteUrl 76 | } 77 | } 78 | } 79 | `, 80 | feeds: [ 81 | { 82 | serialize: ({ query: { site, allMarkdownRemark } }) => { 83 | return allMarkdownRemark.nodes.map(node => { 84 | return Object.assign({}, node.frontmatter, { 85 | description: node.excerpt, 86 | date: node.frontmatter.date, 87 | url: site.siteMetadata.siteUrl + node.fields.slug, 88 | guid: site.siteMetadata.siteUrl + node.fields.slug, 89 | custom_elements: [{ "content:encoded": node.html }], 90 | }) 91 | }) 92 | }, 93 | query: ` 94 | { 95 | allMarkdownRemark( 96 | sort: { order: DESC, fields: [frontmatter___date] }, 97 | ) { 98 | nodes { 99 | excerpt 100 | html 101 | fields { 102 | slug 103 | } 104 | frontmatter { 105 | title 106 | date 107 | } 108 | } 109 | } 110 | } 111 | `, 112 | output: "/rss.xml", 113 | title: "Gatsby Starter Blog RSS Feed", 114 | }, 115 | ], 116 | }, 117 | }, 118 | { 119 | resolve: `gatsby-plugin-manifest`, 120 | options: { 121 | name: `Gatsby Starter Blog`, 122 | short_name: `GatsbyJS`, 123 | start_url: `/`, 124 | background_color: `#ffffff`, 125 | // This will impact how browsers show your PWA/website 126 | // https://css-tricks.com/meta-theme-color-and-trickery/ 127 | // theme_color: `#663399`, 128 | display: `minimal-ui`, 129 | icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 130 | }, 131 | }, 132 | `gatsby-plugin-react-helmet`, 133 | // this (optional) plugin enables Progressive Web App + Offline functionality 134 | // To learn more, visit: https://gatsby.dev/offline 135 | // `gatsby-plugin-offline`, 136 | ], 137 | } 138 | -------------------------------------------------------------------------------- /gatsby/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | const { createFilePath } = require(`gatsby-source-filesystem`) 3 | 4 | exports.createPages = async ({ graphql, actions, reporter }) => { 5 | const { createPage } = actions 6 | 7 | // Define a template for blog post 8 | const blogPost = path.resolve(`./src/templates/blog-post.js`) 9 | 10 | // Get all markdown blog posts sorted by date 11 | const result = await graphql( 12 | ` 13 | { 14 | allMarkdownRemark( 15 | sort: { fields: [frontmatter___date], order: ASC } 16 | limit: 1000 17 | ) { 18 | nodes { 19 | id 20 | fields { 21 | slug 22 | } 23 | } 24 | } 25 | } 26 | ` 27 | ) 28 | 29 | if (result.errors) { 30 | reporter.panicOnBuild( 31 | `There was an error loading your blog posts`, 32 | result.errors 33 | ) 34 | return 35 | } 36 | 37 | const posts = result.data.allMarkdownRemark.nodes 38 | 39 | // Create blog posts pages 40 | // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js) 41 | // `context` is available in the template as a prop and as a variable in GraphQL 42 | 43 | if (posts.length > 0) { 44 | posts.forEach((post, index) => { 45 | const previousPostId = index === 0 ? null : posts[index - 1].id 46 | const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id 47 | 48 | createPage({ 49 | path: post.fields.slug, 50 | component: blogPost, 51 | context: { 52 | id: post.id, 53 | previousPostId, 54 | nextPostId, 55 | }, 56 | }) 57 | }) 58 | } 59 | } 60 | 61 | exports.onCreateNode = ({ node, actions, getNode }) => { 62 | const { createNodeField } = actions 63 | 64 | if (node.internal.type === `MarkdownRemark`) { 65 | const value = createFilePath({ node, getNode }) 66 | 67 | createNodeField({ 68 | name: `slug`, 69 | node, 70 | value, 71 | }) 72 | } 73 | } 74 | 75 | exports.createSchemaCustomization = ({ actions }) => { 76 | const { createTypes } = actions 77 | 78 | // Explicitly define the siteMetadata {} object 79 | // This way those will always be defined even if removed from gatsby-config.js 80 | 81 | // Also explicitly define the Markdown frontmatter 82 | // This way the "MarkdownRemark" queries will return `null` even when no 83 | // blog posts are stored inside "content/blog" instead of returning an error 84 | createTypes(` 85 | type SiteSiteMetadata { 86 | author: Author 87 | siteUrl: String 88 | social: Social 89 | } 90 | 91 | type Author { 92 | name: String 93 | summary: String 94 | } 95 | 96 | type Social { 97 | twitter: String 98 | } 99 | 100 | type MarkdownRemark implements Node { 101 | frontmatter: Frontmatter 102 | fields: Fields 103 | } 104 | 105 | type Frontmatter { 106 | title: String 107 | description: String 108 | date: Date @dateformat 109 | } 110 | 111 | type Fields { 112 | slug: String 113 | } 114 | `) 115 | } 116 | -------------------------------------------------------------------------------- /gatsby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chiselstrike-example-blog", 3 | "private": true, 4 | "description": "An example template for a ChiselStrike blog with Gatsby", 5 | "version": "0.1.0", 6 | "author": "ChiselStrike Beta Team ", 7 | "bugs": { 8 | "url": "https://github.com/gatsbyjs/gatsby/issues" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.27.2", 12 | "date-fns": "^2.28.0", 13 | "gatsby": "^4.16.0", 14 | "gatsby-plugin-feed": "^4.16.0", 15 | "gatsby-plugin-gatsby-cloud": "^4.16.0", 16 | "gatsby-plugin-google-analytics": "^4.16.0", 17 | "gatsby-plugin-image": "^2.16.1", 18 | "gatsby-plugin-manifest": "^4.16.0", 19 | "gatsby-plugin-offline": "^5.16.0", 20 | "gatsby-plugin-react-helmet": "^5.16.0", 21 | "gatsby-plugin-sharp": "^4.16.1", 22 | "gatsby-remark-copy-linked-files": "^5.16.0", 23 | "gatsby-remark-images": "^6.16.0", 24 | "gatsby-remark-prismjs": "^6.16.0", 25 | "gatsby-remark-responsive-iframe": "^5.16.0", 26 | "gatsby-remark-smartypants": "^5.16.0", 27 | "gatsby-source-filesystem": "^4.16.0", 28 | "gatsby-transformer-remark": "^5.16.0", 29 | "gatsby-transformer-sharp": "^4.16.0", 30 | "http-proxy-middleware": "^2.0.6", 31 | "prismjs": "^1.28.0", 32 | "react": "^18.1.0", 33 | "react-dom": "^18.1.0", 34 | "react-helmet": "^6.1.0", 35 | "typeface-merriweather": "1.1.13", 36 | "typeface-montserrat": "1.1.13" 37 | }, 38 | "devDependencies": { 39 | "autoprefixer": "^10.4.7", 40 | "gatsby-plugin-postcss": "^5.16.0", 41 | "postcss": "^8.4.14", 42 | "prettier": "^2.6.2", 43 | "tailwindcss": "^3.1.0" 44 | }, 45 | "homepage": "https://github.com/gatsbyjs/gatsby-starter-blog#readme", 46 | "keywords": [ 47 | "gatsby" 48 | ], 49 | "license": "0BSD", 50 | "main": "n/a", 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/gatsbyjs/gatsby-starter-blog.git" 54 | }, 55 | "scripts": { 56 | "postinstall": "cd backend && npm install", 57 | "build": "gatsby build", 58 | "develop": "gatsby develop", 59 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 60 | "start": "gatsby develop", 61 | "serve": "gatsby serve", 62 | "clean": "gatsby clean", 63 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gatsby/plugins/gatsby-chisel/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process") 2 | 3 | const consoleColorOff = "\x1b[0m" 4 | const consoleColorWhite = "\x1b[37m" 5 | const consoleColorRed = "\x1b[31m" 6 | const consoleColorCyan = "\x1b[36m" 7 | 8 | function color(color, msg) { 9 | return `${color}${msg}${consoleColorOff} ` 10 | } 11 | 12 | function logNormalData(data) { 13 | String(data) 14 | .split("\n") 15 | .forEach(s => 16 | console.log( 17 | color(consoleColorCyan, "ChiselStrike:"), 18 | color(consoleColorWhite, s) 19 | ) 20 | ) 21 | } 22 | 23 | exports.pluginOptionsSchema = ({ Joi }) => { 24 | return Joi.object({ 25 | path: Joi.string().required(), 26 | }) 27 | } 28 | 29 | exports.onCreateDevServer = (_, options) => { 30 | const chiselServer = spawn("npm", ["run", "dev"], { 31 | cwd: options.path, 32 | }) 33 | 34 | chiselServer.stdout.on("data", data => { 35 | logNormalData(data) 36 | }) 37 | 38 | chiselServer.stderr.on("data", data => { 39 | logNormalData(data) 40 | }) 41 | 42 | chiselServer.on("error", error => { 43 | console.error( 44 | color(consoleColorRed, `ChiselStrike error: ${error.message}`) 45 | ) 46 | }) 47 | 48 | chiselServer.on("close", code => { 49 | console.log( 50 | color( 51 | consoleColorRed, 52 | `ChiselStrike's server process exited with code ${code}. Killing Gatsby's process soon...` 53 | ) 54 | ) 55 | // Kills Gatsby along with ChiselStrike, remove this if ChiselStrike needs to fail silently 56 | process.exit(code ?? 1) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /gatsby/plugins/gatsby-chisel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-plugin-chisel", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "gatsby-node.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /gatsby/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /gatsby/src/components/comment-form.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react" 2 | 3 | import api from "../services/api" 4 | import { getCommentsFromChisel } from "./comment-section" 5 | 6 | export default function CommentForm({ postId, setIsLoading, setComments }) { 7 | const [newComment, setNewComment] = useState("") 8 | 9 | const handleCommentCreation = useCallback(async () => { 10 | setIsLoading(true) 11 | try { 12 | await api.post("comments", { 13 | content: newComment, 14 | postId: postId, 15 | }) 16 | 17 | setComments(await getCommentsFromChisel(postId)) 18 | } catch (error) { 19 | console.error(error) 20 | } 21 | 22 | setNewComment("") 23 | setIsLoading(false) 24 | }, [postId, setIsLoading, setComments, newComment]) 25 | 26 | const handleResetComment = useCallback(() => { 27 | setNewComment("") 28 | }, []) 29 | 30 | const handleChangeNewComment = useCallback(event => { 31 | setNewComment(event.target.value) 32 | }, []) 33 | 34 | return ( 35 |
36 |

Leave a Comment

37 |
38 | 39 | {" "} 40 | Type your Comment Below 41 | 42 |
43 |
44 | 51 |
52 |
53 |
54 | 57 | 60 |
61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /gatsby/src/components/comment-section.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | 3 | import Comment from "./comment" 4 | import CommentForm from "./comment-form" 5 | import Loader from "./loader" 6 | 7 | import api from "../services/api" 8 | 9 | export const getCommentsFromChisel = async postId => { 10 | try { 11 | const res = await api.get("comments", { 12 | params: { 13 | postId, 14 | }, 15 | }) 16 | return res.data 17 | } catch (error) { 18 | console.error(error) 19 | } 20 | } 21 | 22 | export default function CommentSection({ postId }) { 23 | const [comments, setComments] = useState([]) 24 | const [isLoading, setIsLoading] = useState(false) 25 | 26 | useEffect(() => { 27 | const getComments = async () => { 28 | setComments(await getCommentsFromChisel(postId)) 29 | } 30 | getComments() 31 | }, [postId]) 32 | 33 | return ( 34 |
35 | {isLoading && } 36 | 41 |
42 |

43 | Comments: 44 |

45 | {comments?.map(comm => ( 46 | 53 | ))} 54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /gatsby/src/components/comment.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { formatDistance } from "date-fns" 3 | 4 | const now = new Date() 5 | 6 | const Comment = ({ id, content, postedAt }) => { 7 | return ( 8 |
9 |
10 |

{`Anonymous said:`}

11 |

12 | {`${formatDistance(new Date(postedAt), now)} ago`} 13 |

14 |
15 |

{content}

16 |
17 | ) 18 | } 19 | 20 | export default Comment 21 | -------------------------------------------------------------------------------- /gatsby/src/components/layout.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link } from "gatsby" 3 | 4 | const Layout = ({ location, title, children }) => { 5 | const rootPath = `${__PATH_PREFIX__}/` 6 | const isRootPath = location.pathname === rootPath 7 | let header 8 | 9 | if (isRootPath) { 10 | header = ( 11 |

12 | {title} 13 |

14 | ) 15 | } else { 16 | header = ( 17 | 18 | {title} 19 | 20 | ) 21 | } 22 | 23 | return ( 24 |
25 |
{header}
26 |
{children}
27 |
28 | © {new Date().getFullYear()}, Built with 29 | {` `} 30 | Gatsby 31 |
32 |
33 | ) 34 | } 35 | 36 | export default Layout 37 | -------------------------------------------------------------------------------- /gatsby/src/components/loader.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | function Loader() { 4 | return ( 5 |
6 |
7 |
8 | ) 9 | } 10 | 11 | export default Loader 12 | -------------------------------------------------------------------------------- /gatsby/src/components/seo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SEO component that queries for data with 3 | * Gatsby's useStaticQuery React hook 4 | * 5 | * See: https://www.gatsbyjs.com/docs/use-static-query/ 6 | */ 7 | 8 | import * as React from "react" 9 | import PropTypes from "prop-types" 10 | import { Helmet } from "react-helmet" 11 | import { useStaticQuery, graphql } from "gatsby" 12 | 13 | const Seo = ({ description, lang, meta, title }) => { 14 | const { site } = useStaticQuery( 15 | graphql` 16 | query { 17 | site { 18 | siteMetadata { 19 | title 20 | description 21 | social { 22 | twitter 23 | } 24 | } 25 | } 26 | } 27 | ` 28 | ) 29 | 30 | const metaDescription = description || site.siteMetadata.description 31 | const defaultTitle = site.siteMetadata?.title 32 | 33 | return ( 34 | 75 | ) 76 | } 77 | 78 | Seo.defaultProps = { 79 | lang: `en`, 80 | meta: [], 81 | description: ``, 82 | } 83 | 84 | Seo.propTypes = { 85 | description: PropTypes.string, 86 | lang: PropTypes.string, 87 | meta: PropTypes.arrayOf(PropTypes.object), 88 | title: PropTypes.string.isRequired, 89 | } 90 | 91 | export default Seo 92 | -------------------------------------------------------------------------------- /gatsby/src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiselstrike/chiselstrike-examples/78f7f0374155a3cdcc3a3db1502c69f00a2a8e02/gatsby/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /gatsby/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { graphql } from "gatsby" 3 | 4 | import Layout from "../components/layout" 5 | import Seo from "../components/seo" 6 | 7 | const NotFoundPage = ({ data, location }) => { 8 | const siteTitle = data.site.siteMetadata.title 9 | 10 | return ( 11 | 12 | 13 |

404: Not Found

14 |

You just hit a route that doesn't exist... the sadness.

15 |
16 | ) 17 | } 18 | 19 | export default NotFoundPage 20 | 21 | export const pageQuery = graphql` 22 | query { 23 | site { 24 | siteMetadata { 25 | title 26 | } 27 | } 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /gatsby/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Link, graphql } from "gatsby" 3 | 4 | import Layout from "../components/layout" 5 | import Seo from "../components/seo" 6 | 7 | const BlogIndex = ({ data, location }) => { 8 | const siteTitle = data.site.siteMetadata?.title || `Title` 9 | const posts = data.allMarkdownRemark.nodes 10 | 11 | if (posts.length === 0) { 12 | return ( 13 | 14 | 15 |

16 | No blog posts found. Add markdown posts to "content/blog" (or the 17 | directory you specified for the "gatsby-source-filesystem" plugin in 18 | gatsby-config.js). 19 |

20 |
21 | ) 22 | } 23 | 24 | return ( 25 | 26 | 27 |
    28 | {posts.map(post => { 29 | const title = post.frontmatter.title || post.fields.slug 30 | 31 | return ( 32 |
  1. 33 |
    38 |
    39 |

    40 | 41 | {title} 42 | 43 |

    44 | {post.frontmatter.date} 45 |
    46 |
    47 |

    53 |

    54 |
    55 |
  2. 56 | ) 57 | })} 58 |
59 |
60 | ) 61 | } 62 | 63 | export default BlogIndex 64 | 65 | export const pageQuery = graphql` 66 | query { 67 | site { 68 | siteMetadata { 69 | title 70 | } 71 | } 72 | allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) { 73 | nodes { 74 | excerpt 75 | fields { 76 | slug 77 | } 78 | frontmatter { 79 | date(formatString: "MMMM DD, YYYY") 80 | title 81 | description 82 | } 83 | } 84 | } 85 | } 86 | ` 87 | -------------------------------------------------------------------------------- /gatsby/src/pages/using-typescript.tsx: -------------------------------------------------------------------------------- 1 | // If you don't want to use TypeScript you can delete this file! 2 | import * as React from "react" 3 | import { PageProps, Link, graphql } from "gatsby" 4 | 5 | import Layout from "../components/layout" 6 | import Seo from "../components/seo" 7 | 8 | type DataProps = { 9 | site: { 10 | buildTime: string 11 | } 12 | } 13 | 14 | const UsingTypescript: React.FC> = ({ 15 | data, 16 | path, 17 | location, 18 | }) => ( 19 | 20 | 21 |

Gatsby supports TypeScript by default!

22 |

23 | This means that you can create and write .ts/.tsx files for your 24 | pages, components etc. Please note that the gatsby-*.js files 25 | (like gatsby-node.js) currently don't support TypeScript yet. 26 |

27 |

28 | For type checking you'll want to install typescript via npm and 29 | run tsc --init to create a tsconfig file. 30 |

31 |

32 | You're currently on the page "{path}" which was built on{" "} 33 | {data.site.buildTime}. 34 |

35 |

36 | To learn more, head over to our{" "} 37 | 38 | documentation about TypeScript 39 | 40 | . 41 |

42 | Go back to the homepage 43 |
44 | ) 45 | 46 | export default UsingTypescript 47 | 48 | export const query = graphql` 49 | { 50 | site { 51 | buildTime(formatString: "YYYY-MM-DD hh:mm a z") 52 | } 53 | } 54 | ` 55 | -------------------------------------------------------------------------------- /gatsby/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const chiselStrikeApi = axios.create({ 4 | baseURL: "http://localhost:8080/dev", 5 | }) 6 | 7 | export default chiselStrikeApi 8 | -------------------------------------------------------------------------------- /gatsby/src/styles/global.css: -------------------------------------------------------------------------------- 1 | /* CSS Custom Properties Definitions */ 2 | 3 | :root { 4 | --maxWidth-none: "none"; 5 | --maxWidth-xs: 20rem; 6 | --maxWidth-sm: 24rem; 7 | --maxWidth-md: 28rem; 8 | --maxWidth-lg: 32rem; 9 | --maxWidth-xl: 36rem; 10 | --maxWidth-2xl: 42rem; 11 | --maxWidth-3xl: 48rem; 12 | --maxWidth-4xl: 56rem; 13 | --maxWidth-full: "100%"; 14 | --maxWidth-wrapper: var(--maxWidth-2xl); 15 | --spacing-px: "1px"; 16 | --spacing-0: 0; 17 | --spacing-1: 0.25rem; 18 | --spacing-2: 0.5rem; 19 | --spacing-3: 0.75rem; 20 | --spacing-4: 1rem; 21 | --spacing-5: 1.25rem; 22 | --spacing-6: 1.5rem; 23 | --spacing-8: 2rem; 24 | --spacing-10: 2.5rem; 25 | --spacing-12: 3rem; 26 | --spacing-16: 4rem; 27 | --spacing-20: 5rem; 28 | --spacing-24: 6rem; 29 | --spacing-32: 8rem; 30 | --fontFamily-sans: Montserrat, system-ui, -apple-system, BlinkMacSystemFont, 31 | "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 32 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 33 | --fontFamily-serif: "Merriweather", "Georgia", Cambria, "Times New Roman", 34 | Times, serif; 35 | --font-body: var(--fontFamily-serif); 36 | --font-heading: var(--fontFamily-sans); 37 | --fontWeight-normal: 400; 38 | --fontWeight-medium: 500; 39 | --fontWeight-semibold: 600; 40 | --fontWeight-bold: 700; 41 | --fontWeight-extrabold: 800; 42 | --fontWeight-black: 900; 43 | --fontSize-root: 16px; 44 | --lineHeight-none: 1; 45 | --lineHeight-tight: 1.1; 46 | --lineHeight-normal: 1.5; 47 | --lineHeight-relaxed: 1.625; 48 | /* 1.200 Minor Third Type Scale */ 49 | --fontSize-0: 0.833rem; 50 | --fontSize-1: 1rem; 51 | --fontSize-2: 1.2rem; 52 | --fontSize-3: 1.44rem; 53 | --fontSize-4: 1.728rem; 54 | --fontSize-5: 2.074rem; 55 | --fontSize-6: 2.488rem; 56 | --fontSize-7: 2.986rem; 57 | --color-primary: #005b99; 58 | --color-text: #2e353f; 59 | --color-text-light: #4f5969; 60 | --color-heading: #1a202c; 61 | --color-heading-black: black; 62 | --color-accent: #d1dce5; 63 | } 64 | 65 | /* HTML elements */ 66 | 67 | *, 68 | :after, 69 | :before { 70 | box-sizing: border-box; 71 | } 72 | 73 | html { 74 | line-height: var(--lineHeight-normal); 75 | font-size: var(--fontSize-root); 76 | -webkit-font-smoothing: antialiased; 77 | -moz-osx-font-smoothing: grayscale; 78 | } 79 | 80 | body { 81 | font-family: var(--font-body); 82 | font-size: var(--fontSize-1); 83 | color: var(--color-text); 84 | } 85 | 86 | footer { 87 | padding: var(--spacing-6) var(--spacing-0); 88 | } 89 | 90 | hr { 91 | background: var(--color-accent); 92 | height: 1px; 93 | border: 0; 94 | } 95 | 96 | /* Heading */ 97 | 98 | h1, 99 | h2, 100 | h3, 101 | h4, 102 | h5, 103 | h6 { 104 | font-family: var(--font-heading); 105 | margin-top: var(--spacing-12); 106 | margin-bottom: var(--spacing-6); 107 | line-height: var(--lineHeight-tight); 108 | letter-spacing: -0.025em; 109 | } 110 | 111 | h2, 112 | h3, 113 | h4, 114 | h5, 115 | h6 { 116 | font-weight: var(--fontWeight-bold); 117 | color: var(--color-heading); 118 | } 119 | 120 | h1 { 121 | font-weight: var(--fontWeight-black); 122 | font-size: var(--fontSize-6); 123 | color: var(--color-heading-black); 124 | } 125 | 126 | h2 { 127 | font-size: var(--fontSize-5); 128 | } 129 | 130 | h3 { 131 | font-size: var(--fontSize-4); 132 | } 133 | 134 | h4 { 135 | font-size: var(--fontSize-3); 136 | } 137 | 138 | h5 { 139 | font-size: var(--fontSize-2); 140 | } 141 | 142 | h6 { 143 | font-size: var(--fontSize-1); 144 | } 145 | 146 | h1 > a { 147 | color: inherit; 148 | text-decoration: none; 149 | } 150 | 151 | h2 > a, 152 | h3 > a, 153 | h4 > a, 154 | h5 > a, 155 | h6 > a { 156 | text-decoration: none; 157 | color: inherit; 158 | } 159 | 160 | /* Prose */ 161 | 162 | p { 163 | line-height: var(--lineHeight-relaxed); 164 | --baseline-multiplier: 0.179; 165 | --x-height-multiplier: 0.35; 166 | margin: var(--spacing-0) var(--spacing-0) var(--spacing-8) var(--spacing-0); 167 | padding: var(--spacing-0); 168 | } 169 | 170 | ul, 171 | ol { 172 | margin-left: var(--spacing-0); 173 | margin-right: var(--spacing-0); 174 | padding: var(--spacing-0); 175 | margin-bottom: var(--spacing-8); 176 | list-style-position: outside; 177 | list-style-image: none; 178 | } 179 | 180 | ul li, 181 | ol li { 182 | padding-left: var(--spacing-0); 183 | margin-bottom: calc(var(--spacing-8) / 2); 184 | } 185 | 186 | li > p { 187 | margin-bottom: calc(var(--spacing-8) / 2); 188 | } 189 | 190 | li *:last-child { 191 | margin-bottom: var(--spacing-0); 192 | } 193 | 194 | li > ul { 195 | margin-left: var(--spacing-8); 196 | margin-top: calc(var(--spacing-8) / 2); 197 | } 198 | 199 | blockquote { 200 | color: var(--color-text-light); 201 | margin-left: calc(-1 * var(--spacing-6)); 202 | margin-right: var(--spacing-8); 203 | padding: var(--spacing-0) var(--spacing-0) var(--spacing-0) var(--spacing-6); 204 | border-left: var(--spacing-1) solid var(--color-primary); 205 | font-size: var(--fontSize-2); 206 | font-style: italic; 207 | margin-bottom: var(--spacing-8); 208 | } 209 | 210 | blockquote > :last-child { 211 | margin-bottom: var(--spacing-0); 212 | } 213 | 214 | blockquote > ul, 215 | blockquote > ol { 216 | list-style-position: inside; 217 | } 218 | 219 | table { 220 | width: 100%; 221 | margin-bottom: var(--spacing-8); 222 | border-collapse: collapse; 223 | border-spacing: 0.25rem; 224 | } 225 | 226 | table thead tr th { 227 | border-bottom: 1px solid var(--color-accent); 228 | } 229 | 230 | /* Link */ 231 | 232 | a { 233 | color: var(--color-primary); 234 | } 235 | 236 | a:hover, 237 | a:focus { 238 | text-decoration: none; 239 | } 240 | 241 | /* Custom classes */ 242 | 243 | .global-wrapper { 244 | margin: var(--spacing-0) auto; 245 | max-width: var(--maxWidth-wrapper); 246 | padding: var(--spacing-10) var(--spacing-5); 247 | } 248 | 249 | .global-wrapper[data-is-root-path="true"] .bio { 250 | margin-bottom: var(--spacing-20); 251 | } 252 | 253 | .global-header { 254 | margin-bottom: var(--spacing-12); 255 | } 256 | 257 | .main-heading { 258 | font-size: var(--fontSize-7); 259 | margin: 0; 260 | } 261 | 262 | .post-list-item { 263 | margin-bottom: var(--spacing-8); 264 | margin-top: var(--spacing-8); 265 | } 266 | 267 | .post-list-item p { 268 | margin-bottom: var(--spacing-0); 269 | } 270 | 271 | .post-list-item h2 { 272 | font-size: var(--fontSize-4); 273 | color: var(--color-primary); 274 | margin-bottom: var(--spacing-2); 275 | margin-top: var(--spacing-0); 276 | } 277 | 278 | .post-list-item header { 279 | margin-bottom: var(--spacing-4); 280 | } 281 | 282 | .header-link-home { 283 | font-weight: var(--fontWeight-bold); 284 | font-family: var(--font-heading); 285 | text-decoration: none; 286 | font-size: var(--fontSize-2); 287 | } 288 | 289 | .blog-post header h1 { 290 | margin: var(--spacing-0) var(--spacing-0) var(--spacing-4) var(--spacing-0); 291 | font-size: var(--fontSize-5); 292 | } 293 | 294 | .blog-post header p { 295 | font-size: var(--fontSize-2); 296 | font-family: var(--font-heading); 297 | margin-bottom: var(--spacing-3); 298 | } 299 | 300 | .blog-post a { 301 | color: var(--color-text-light); 302 | transition: opacity 0.3s ease; 303 | } 304 | 305 | .blog-post a:hover { 306 | opacity: 0.6; 307 | } 308 | 309 | .blog-post-nav ul { 310 | margin-top: var(--spacing-6); 311 | } 312 | 313 | .gatsby-highlight { 314 | margin-bottom: var(--spacing-8); 315 | } 316 | 317 | /* Media queries */ 318 | 319 | @media (max-width: 42rem) { 320 | blockquote { 321 | padding: var(--spacing-0) var(--spacing-0) var(--spacing-0) var(--spacing-4); 322 | margin-left: var(--spacing-0); 323 | } 324 | ul, 325 | ol { 326 | list-style-position: inside; 327 | } 328 | } 329 | 330 | /* CSS Loader */ 331 | .loader { 332 | color: #ffffff; 333 | font-size: 90px; 334 | text-indent: -9999em; 335 | overflow: hidden; 336 | width: 1em; 337 | height: 1em; 338 | border-radius: 50%; 339 | margin: 72px auto; 340 | position: relative; 341 | -webkit-transform: translateZ(0); 342 | -ms-transform: translateZ(0); 343 | transform: translateZ(0); 344 | -webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease; 345 | animation: load6 1.7s infinite ease, round 1.7s infinite ease; 346 | } 347 | @-webkit-keyframes load6 { 348 | 0% { 349 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 350 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 351 | } 352 | 5%, 353 | 95% { 354 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 355 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 356 | } 357 | 10%, 358 | 59% { 359 | box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, 360 | -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, 361 | -0.297em -0.775em 0 -0.477em; 362 | } 363 | 20% { 364 | box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, 365 | -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, 366 | -0.749em -0.34em 0 -0.477em; 367 | } 368 | 38% { 369 | box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, 370 | -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, 371 | -0.82em -0.09em 0 -0.477em; 372 | } 373 | 100% { 374 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 375 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 376 | } 377 | } 378 | @keyframes load6 { 379 | 0% { 380 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 381 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 382 | } 383 | 5%, 384 | 95% { 385 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 386 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 387 | } 388 | 10%, 389 | 59% { 390 | box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, 391 | -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, 392 | -0.297em -0.775em 0 -0.477em; 393 | } 394 | 20% { 395 | box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, 396 | -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, 397 | -0.749em -0.34em 0 -0.477em; 398 | } 399 | 38% { 400 | box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, 401 | -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, 402 | -0.82em -0.09em 0 -0.477em; 403 | } 404 | 100% { 405 | box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 406 | 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em; 407 | } 408 | } 409 | @-webkit-keyframes round { 410 | 0% { 411 | -webkit-transform: rotate(0deg); 412 | transform: rotate(0deg); 413 | } 414 | 100% { 415 | -webkit-transform: rotate(360deg); 416 | transform: rotate(360deg); 417 | } 418 | } 419 | @keyframes round { 420 | 0% { 421 | -webkit-transform: rotate(0deg); 422 | transform: rotate(0deg); 423 | } 424 | 100% { 425 | -webkit-transform: rotate(360deg); 426 | transform: rotate(360deg); 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /gatsby/src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type="button"], 199 | [type="reset"], 200 | [type="submit"] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type="button"]:-moz-focusring, 222 | [type="reset"]:-moz-focusring, 223 | [type="submit"]:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type="checkbox"], 273 | [type="radio"] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type="number"]::-webkit-inner-spin-button, 283 | [type="number"]::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type="search"] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10. 339 | */ 340 | 341 | [hidden] { 342 | display: none; 343 | } 344 | -------------------------------------------------------------------------------- /gatsby/src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .common-button { 7 | @apply transition-colors bg-white mr-2 border-2 p-3 shadow-sm 8 | rounded text-sm font-semibold hover:bg-slate-600 hover:text-white; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gatsby/src/templates/blog-post.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link, graphql } from "gatsby" 3 | 4 | import Layout from "../components/layout" 5 | import Seo from "../components/seo" 6 | import CommentSection from "../components/comment-section" 7 | 8 | 9 | const BlogPostTemplate = ({ data, location }) => { 10 | const post = data.markdownRemark 11 | const siteTitle = data.site.siteMetadata?.title || `Title` 12 | const { previous, next } = data 13 | 14 | return ( 15 | 16 | 20 |
25 |
26 |

{post.frontmatter.title}

27 |

{post.frontmatter.date}

28 |
29 |
33 |
34 | 35 | 61 | 62 | 63 |
64 | ) 65 | } 66 | 67 | export default BlogPostTemplate 68 | 69 | export const pageQuery = graphql` 70 | query BlogPostBySlug( 71 | $id: String! 72 | $previousPostId: String 73 | $nextPostId: String 74 | ) { 75 | site { 76 | siteMetadata { 77 | title 78 | } 79 | } 80 | markdownRemark(id: { eq: $id }) { 81 | id 82 | excerpt(pruneLength: 160) 83 | html 84 | frontmatter { 85 | title 86 | date(formatString: "MMMM DD, YYYY") 87 | description 88 | } 89 | } 90 | previous: markdownRemark(id: { eq: $previousPostId }) { 91 | fields { 92 | slug 93 | } 94 | frontmatter { 95 | title 96 | } 97 | } 98 | next: markdownRemark(id: { eq: $nextPostId }) { 99 | fields { 100 | slug 101 | } 102 | frontmatter { 103 | title 104 | } 105 | } 106 | } 107 | ` 108 | -------------------------------------------------------------------------------- /gatsby/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiselstrike/chiselstrike-examples/78f7f0374155a3cdcc3a3db1502c69f00a2a8e02/gatsby/static/favicon.ico -------------------------------------------------------------------------------- /gatsby/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /gatsby/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /nextjs/.env.local.sample: -------------------------------------------------------------------------------- 1 | OAUTH_GHID=your_oauth_app_id_here -------------------------------------------------------------------------------- /nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # ChiselStrike 37 | chiseld-data.db* 38 | chiseld.db* -------------------------------------------------------------------------------- /nextjs/Chisel.toml: -------------------------------------------------------------------------------- 1 | models = ["types"] 2 | endpoints = ["endpoints"] 3 | policies = ["policies"] 4 | modules = "deno" -------------------------------------------------------------------------------- /nextjs/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | After the first checkout, run `npm install`. 4 | 5 | To start the frontend server, run `npm run dev` in this directory. 6 | 7 | In another window, run `chisel dev` in this directory. 8 | 9 | Visit http://localhost:3000 10 | 11 | ## OAuth Setup 12 | 13 | Please contact us for instructions. 14 | -------------------------------------------------------------------------------- /nextjs/endpoints/get_all_people.js: -------------------------------------------------------------------------------- 1 | import { responseFromJson } from "@chiselstrike/api" 2 | 3 | export default async function chisel(req) { 4 | if (req.method == 'GET') { 5 | try { 6 | let resp_json = []; 7 | await Person.cursor().forEach(p => resp_json.push(p)) 8 | return responseFromJson(resp_json); 9 | } catch (e) { 10 | return responseFromJson(e, 500); 11 | } 12 | } 13 | return responseFromJson("Only GET is allowed", 405); 14 | } 15 | -------------------------------------------------------------------------------- /nextjs/endpoints/import_person.js: -------------------------------------------------------------------------------- 1 | import { responseFromJson } from "@chiselstrike/api" 2 | 3 | export default async function chisel(req) { 4 | if (req.method == 'PUT') { 5 | try { 6 | await Person.build(await req.json()).save(); 7 | return responseFromJson("ok"); 8 | } catch (e) { 9 | return responseFromJson(e, 500); 10 | } 11 | } 12 | return responseFromJson("Only PUT is allowed", 405); 13 | } 14 | -------------------------------------------------------------------------------- /nextjs/lib/withSession.js: -------------------------------------------------------------------------------- 1 | import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next"; 2 | 3 | const sessionOptions = { 4 | password: "complex_password_at_least_32_characters_long", 5 | cookieName: "chiselstrike_cookie10", 6 | // secure: true should be used in production (HTTPS) but can't be used in development (HTTP) 7 | cookieOptions: { 8 | secure: process.env.NODE_ENV === "production", 9 | }, 10 | }; 11 | 12 | export function withSessionRoute(handler) { 13 | return withIronSessionApiRoute(handler, sessionOptions); 14 | } 15 | 16 | export function withSessionSsr(handler) { 17 | return withIronSessionSsr(handler, sessionOptions); 18 | } 19 | -------------------------------------------------------------------------------- /nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-chisel", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@chiselstrike/frontend": "0.4.1", 12 | "iron-session": "^6.0.4", 13 | "next": "^12.0.7", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "swr": "^1.0.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nextjs/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /nextjs/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /nextjs/pages/api/logout.js: -------------------------------------------------------------------------------- 1 | import { withSessionRoute } from "../../lib/withSession"; 2 | 3 | export default withSessionRoute(logoutRoute); 4 | 5 | async function logoutRoute(req, res) { 6 | await req.session.destroy(); 7 | res.send("Logged out"); 8 | } 9 | -------------------------------------------------------------------------------- /nextjs/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Image from 'next/image' 3 | import Link from 'next/link' 4 | import styles from '../styles/Home.module.css' 5 | import React, {useEffect} from "react"; 6 | import { chiselFetch, getChiselStrikeClient } from '@chiselstrike/frontend'; 7 | import { withSessionSsr } from "../lib/withSession"; 8 | 9 | export const getServerSideProps = withSessionSsr( 10 | async function getServerSideProps(context) { 11 | const chisel = await getChiselStrikeClient(context.req.session, context.query); 12 | return { props: { chisel } }; 13 | }, 14 | ); 15 | 16 | export default function Home({ chisel }) { 17 | const [peopleData, setPeopleData] = React.useState([]) 18 | 19 | async function fetch_people() { 20 | const res = await chiselFetch(chisel, 'dev/get_all_people', { 21 | method: 'GET', 22 | }); 23 | const jsonData = await res.json(); 24 | setPeopleData(jsonData) 25 | } 26 | useEffect(fetch_people, []) 27 | const defaultState = { 28 | firstName: "", 29 | lastName: "" 30 | } 31 | 32 | const [state, setState] = React.useState(defaultState) 33 | function handleChange(evt) { 34 | const value = evt.target.value; 35 | setState({ 36 | ...state, 37 | [evt.target.name]: value 38 | }); 39 | } 40 | 41 | const submitPerson = async (event) => { 42 | event.preventDefault() // don't redirect the page 43 | await chiselFetch(chisel, 'dev/import_person', { 44 | method: 'PUT', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | body: JSON.stringify(state), 49 | }); 50 | await fetch_people(); 51 | setState(defaultState) 52 | } 53 | 54 | const greeting = chisel.user ? 55 |

Hello, {chisel.user}. Click here to log out.

: 56 |

Hello, anonymous. Click here to log in.

; 57 | 58 | return ( 59 |
60 | { greeting } 61 |
62 | 71 | 80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | {peopleData.map((person) => ( 89 | 90 | 91 | 92 | 93 | ))} 94 | 95 |
firstNamelastName
{person.firstName}{person.lastName}
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /nextjs/pages/profile.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { getChiselStrikeClient } from "@chiselstrike/frontend"; 3 | import { withSessionSsr } from "../lib/withSession"; 4 | 5 | export const getServerSideProps = withSessionSsr( 6 | async function getServerSideProps(context) { 7 | const chisel = await getChiselStrikeClient(context.req.session, context.query); 8 | return { props: { user: chisel.user, link: chisel.loginLink } }; 9 | }, 10 | ); 11 | 12 | export default function Profile({ user, link }) { 13 | if (user) 14 | return

Profile for user {user}. Log out

; 15 | else { 16 | return

We don't know you. Please log in.

17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nextjs/policies/policies.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiselstrike/chiselstrike-examples/78f7f0374155a3cdcc3a3db1502c69f00a2a8e02/nextjs/policies/policies.yml -------------------------------------------------------------------------------- /nextjs/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /nextjs/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /nextjs/types/types.ts: -------------------------------------------------------------------------------- 1 | import { ChiselEntity } from "@chiselstrike/api" 2 | 3 | export class Person extends ChiselEntity { 4 | firstName: string = ''; 5 | lastName: string = ''; 6 | } 7 | -------------------------------------------------------------------------------- /remix/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | .DS_Store 4 | .env 5 | /.cache 6 | /public/build 7 | /build 8 | -------------------------------------------------------------------------------- /remix/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./data.db?connection_limit=1" 2 | SESSION_SECRET="super-duper-s3cret" 3 | -------------------------------------------------------------------------------- /remix/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').Linter.BaseConfig} 3 | */ 4 | module.exports = { 5 | extends: [ 6 | "@remix-run/eslint-config", 7 | "@remix-run/eslint-config/node", 8 | "@remix-run/eslint-config/jest-testing-library", 9 | "prettier", 10 | ], 11 | // we're using vitest which has a very similar API to jest 12 | // (so the linting plugins work nicely), but it means we have to explicitly 13 | // set the jest version. 14 | settings: { 15 | jest: { 16 | version: 27, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /remix/.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Something is wrong with the Stack. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: >- 7 | Thank you for helping to improve Remix! 8 | 9 | Our bandwidth on maintaining these stacks is limited. As a team, we're 10 | currently focusing our efforts on Remix itself. The good news is you can 11 | fork and adjust this stack however you'd like and start using it today 12 | as a custom stack. Learn more from 13 | [the Remix Stacks docs](https://remix.run/stacks). 14 | 15 | If you'd still like to report a bug, please fill out this form. We can't 16 | promise a timely response, but hopefully when we have the bandwidth to 17 | work on these stacks again we can take a look. Thanks! 18 | 19 | - type: input 20 | attributes: 21 | label: Have you experienced this bug with the latest version of the template? 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Steps to Reproduce 27 | description: Steps to reproduce the behavior. 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Expected Behavior 33 | description: A concise description of what you expected to happen. 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Actual Behavior 39 | description: A concise description of what you're experiencing. 40 | validations: 41 | required: true 42 | -------------------------------------------------------------------------------- /remix/.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/remix-run/remix/discussions/new?category=q-a 5 | about: 6 | If you can't get something to work the way you expect, open a question in 7 | the Remix discussions. 8 | - name: Feature Request 9 | url: https://github.com/remix-run/remix/discussions/new?category=ideas 10 | about: 11 | We appreciate you taking the time to improve Remix with your ideas, but we 12 | use the Remix Discussions for this instead of the issues tab 🙂. 13 | - name: 💬 Remix Discord Channel 14 | url: https://rmx.as/discord 15 | about: Interact with other people using Remix 💿 16 | - name: 💬 New Updates (Twitter) 17 | url: https://twitter.com/remix_run 18 | about: Stay up to date with Remix news on twitter 19 | - name: 🍿 Remix YouTube Channel 20 | url: https://rmx.as/youtube 21 | about: Are you a tech lead or wanting to learn more about Remix in depth? Checkout the Remix YouTube Channel 22 | -------------------------------------------------------------------------------- /remix/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /remix/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | pull_request: {} 8 | permissions: 9 | actions: write 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | name: ⬣ ESLint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 🛑 Cancel Previous Runs 18 | uses: styfle/cancel-workflow-action@0.9.1 19 | 20 | - name: ⬇️ Checkout repo 21 | uses: actions/checkout@v3 22 | 23 | - name: ⎔ Setup node 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | 28 | - name: 📥 Download deps 29 | uses: bahmutov/npm-install@v1 30 | with: 31 | useLockFile: false 32 | 33 | - name: 🔬 Lint 34 | run: npm run lint 35 | 36 | typecheck: 37 | name: ʦ TypeScript 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: 🛑 Cancel Previous Runs 41 | uses: styfle/cancel-workflow-action@0.9.1 42 | 43 | - name: ⬇️ Checkout repo 44 | uses: actions/checkout@v3 45 | 46 | - name: ⎔ Setup node 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: 16 50 | 51 | - name: 📥 Download deps 52 | uses: bahmutov/npm-install@v1 53 | with: 54 | useLockFile: false 55 | 56 | - name: 🔎 Type check 57 | run: npm run typecheck --if-present 58 | 59 | vitest: 60 | name: ⚡ Vitest 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: 🛑 Cancel Previous Runs 64 | uses: styfle/cancel-workflow-action@0.9.1 65 | 66 | - name: ⬇️ Checkout repo 67 | uses: actions/checkout@v3 68 | 69 | - name: ⎔ Setup node 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: 16 73 | 74 | - name: 📥 Download deps 75 | uses: bahmutov/npm-install@v1 76 | with: 77 | useLockFile: false 78 | 79 | - name: ⚡ Run vitest 80 | run: npm run test -- --coverage 81 | 82 | cypress: 83 | name: ⚫️ Cypress 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: 🛑 Cancel Previous Runs 87 | uses: styfle/cancel-workflow-action@0.9.1 88 | 89 | - name: ⬇️ Checkout repo 90 | uses: actions/checkout@v3 91 | 92 | - name: 🏄 Copy test env vars 93 | run: cp .env.example .env 94 | 95 | - name: ⎔ Setup node 96 | uses: actions/setup-node@v3 97 | with: 98 | node-version: 16 99 | 100 | - name: 📥 Download deps 101 | uses: bahmutov/npm-install@v1 102 | with: 103 | useLockFile: false 104 | 105 | - name: 🛠 Setup Database 106 | run: npx prisma migrate reset --force 107 | 108 | - name: ⚙️ Build 109 | run: npm run build 110 | 111 | - name: 🌳 Cypress run 112 | uses: cypress-io/github-action@v4 113 | with: 114 | start: npm run start:mocks 115 | wait-on: "http://localhost:8811" 116 | env: 117 | PORT: "8811" 118 | 119 | build: 120 | name: 🐳 Build 121 | # only build/deploy main branch on pushes 122 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 123 | runs-on: ubuntu-latest 124 | steps: 125 | - name: 🛑 Cancel Previous Runs 126 | uses: styfle/cancel-workflow-action@0.9.1 127 | 128 | - name: ⬇️ Checkout repo 129 | uses: actions/checkout@v3 130 | 131 | - name: 👀 Read app name 132 | uses: SebRollen/toml-action@v1.0.0 133 | id: app_name 134 | with: 135 | file: "fly.toml" 136 | field: "app" 137 | 138 | - name: 🐳 Set up Docker Buildx 139 | uses: docker/setup-buildx-action@v2 140 | 141 | # Setup cache 142 | - name: ⚡️ Cache Docker layers 143 | uses: actions/cache@v3 144 | with: 145 | path: /tmp/.buildx-cache 146 | key: ${{ runner.os }}-buildx-${{ github.sha }} 147 | restore-keys: | 148 | ${{ runner.os }}-buildx- 149 | 150 | - name: 🔑 Fly Registry Auth 151 | uses: docker/login-action@v2 152 | with: 153 | registry: registry.fly.io 154 | username: x 155 | password: ${{ secrets.FLY_API_TOKEN }} 156 | 157 | - name: 🐳 Docker build 158 | uses: docker/build-push-action@v3 159 | with: 160 | context: . 161 | push: true 162 | tags: registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }} 163 | build-args: | 164 | COMMIT_SHA=${{ github.sha }} 165 | cache-from: type=local,src=/tmp/.buildx-cache 166 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new 167 | 168 | # This ugly bit is necessary if you don't want your cache to grow forever 169 | # till it hits GitHub's limit of 5GB. 170 | # Temp fix 171 | # https://github.com/docker/build-push-action/issues/252 172 | # https://github.com/moby/buildkit/issues/1896 173 | - name: 🚚 Move cache 174 | run: | 175 | rm -rf /tmp/.buildx-cache 176 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 177 | 178 | deploy: 179 | name: 🚀 Deploy 180 | runs-on: ubuntu-latest 181 | needs: [lint, typecheck, vitest, cypress, build] 182 | # only build/deploy main branch on pushes 183 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 184 | 185 | steps: 186 | - name: 🛑 Cancel Previous Runs 187 | uses: styfle/cancel-workflow-action@0.9.1 188 | 189 | - name: ⬇️ Checkout repo 190 | uses: actions/checkout@v3 191 | 192 | - name: 👀 Read app name 193 | uses: SebRollen/toml-action@v1.0.0 194 | id: app_name 195 | with: 196 | file: "fly.toml" 197 | field: "app" 198 | 199 | - name: 🚀 Deploy Staging 200 | if: ${{ github.ref == 'refs/heads/dev' }} 201 | uses: superfly/flyctl-actions@1.3 202 | with: 203 | args: "deploy --app ${{ steps.app_name.outputs.value }}-staging --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" 204 | env: 205 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 206 | 207 | - name: 🚀 Deploy Production 208 | if: ${{ github.ref == 'refs/heads/main' }} 209 | uses: superfly/flyctl-actions@1.3 210 | with: 211 | args: "deploy --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" 212 | env: 213 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 214 | -------------------------------------------------------------------------------- /remix/.gitignore: -------------------------------------------------------------------------------- 1 | # We don't want lockfiles in stacks, as people could use a different package manager 2 | # This part will be removed by `remix.init` 3 | package-lock.json 4 | yarn.lock 5 | pnpm-lock.yaml 6 | pnpm-lock.yml 7 | 8 | node_modules 9 | 10 | /build 11 | /public/build 12 | .env 13 | 14 | /cypress/screenshots 15 | /cypress/videos 16 | /prisma/data.db 17 | /prisma/data.db-journal 18 | 19 | /app/styles/tailwind.css 20 | -------------------------------------------------------------------------------- /remix/.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | # Install Fly 4 | RUN curl -L https://fly.io/install.sh | sh 5 | ENV FLYCTL_INSTALL="/home/gitpod/.fly" 6 | ENV PATH="$FLYCTL_INSTALL/bin:$PATH" 7 | 8 | # Install GitHub CLI 9 | RUN brew install gh 10 | -------------------------------------------------------------------------------- /remix/.gitpod.yml: -------------------------------------------------------------------------------- 1 | # https://www.gitpod.io/docs/config-gitpod-file 2 | 3 | image: 4 | file: .gitpod.Dockerfile 5 | 6 | ports: 7 | - port: 3000 8 | onOpen: notify 9 | 10 | tasks: 11 | - name: Restore .env file 12 | command: | 13 | if [ -f .env ]; then 14 | # If this workspace already has a .env, don't override it 15 | # Local changes survive a workspace being opened and closed 16 | # but they will not persist between separate workspaces for the same repo 17 | 18 | echo "Found .env in workspace" 19 | else 20 | # There is no .env 21 | if [ ! -n "${ENV}" ]; then 22 | # There is no $ENV from a previous workspace 23 | # Default to the example .env 24 | echo "Setting example .env" 25 | 26 | cp .env.example .env 27 | else 28 | # After making changes to .env, run this line to persist it to $ENV 29 | # eval $(gp env -e ENV="$(base64 .env | tr -d '\n')") 30 | # 31 | # Environment variables set this way are shared between all your workspaces for this repo 32 | # The lines below will read $ENV and print a .env file 33 | 34 | echo "Restoring .env from Gitpod" 35 | 36 | echo "${ENV}" | base64 -d | tee .env > /dev/null 37 | fi 38 | fi 39 | 40 | - init: npm install 41 | command: npm run setup && npm run dev 42 | 43 | vscode: 44 | extensions: 45 | - ms-azuretools.vscode-docker 46 | - esbenp.prettier-vscode 47 | - dbaeumer.vscode-eslint 48 | - bradlc.vscode-tailwindcss 49 | -------------------------------------------------------------------------------- /remix/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /remix/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /public/build 5 | .env 6 | 7 | /app/styles/tailwind.css 8 | -------------------------------------------------------------------------------- /remix/Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:16-bullseye-slim as base 3 | 4 | # set for base and all layer that inherit from it 5 | ENV NODE_ENV production 6 | 7 | # Install openssl for Prisma 8 | RUN apt-get update && apt-get install -y openssl sqlite3 9 | 10 | # Install all node_modules, including dev dependencies 11 | FROM base as deps 12 | 13 | WORKDIR /myapp 14 | 15 | ADD package.json .npmrc ./ 16 | RUN npm install --production=false 17 | 18 | # Setup production node_modules 19 | FROM base as production-deps 20 | 21 | WORKDIR /myapp 22 | 23 | COPY --from=deps /myapp/node_modules /myapp/node_modules 24 | ADD package.json .npmrc ./ 25 | RUN npm prune --production 26 | 27 | # Build the app 28 | FROM base as build 29 | 30 | WORKDIR /myapp 31 | 32 | COPY --from=deps /myapp/node_modules /myapp/node_modules 33 | 34 | ADD prisma . 35 | RUN npx prisma generate 36 | 37 | ADD . . 38 | RUN npm run build 39 | 40 | # Finally, build the production image with minimal footprint 41 | FROM base 42 | 43 | ENV DATABASE_URL=file:/data/sqlite.db 44 | ENV PORT="8080" 45 | ENV NODE_ENV="production" 46 | 47 | # add shortcut for connecting to database CLI 48 | RUN echo "#!/bin/sh\nset -x\nsqlite3 \$DATABASE_URL" > /usr/local/bin/database-cli && chmod +x /usr/local/bin/database-cli 49 | 50 | WORKDIR /myapp 51 | 52 | COPY --from=production-deps /myapp/node_modules /myapp/node_modules 53 | COPY --from=build /myapp/node_modules/.prisma /myapp/node_modules/.prisma 54 | 55 | COPY --from=build /myapp/build /myapp/build 56 | COPY --from=build /myapp/public /myapp/public 57 | COPY --from=build /myapp/package.json /myapp/package.json 58 | COPY --from=build /myapp/start.sh /myapp/start.sh 59 | COPY --from=build /myapp/prisma /myapp/prisma 60 | 61 | ENTRYPOINT [ "./start.sh" ] 62 | -------------------------------------------------------------------------------- /remix/README.md: -------------------------------------------------------------------------------- 1 | # ChiselStrike Remix example 2 | 3 | This is an example showing how ChiselStrike can be used with Remix' blog post example. 4 | This README.md file will evolve as you go through the commits. 5 | 6 | For now, the project was initialized with the remix stack initialization according to [the Remix tutorial](https://remix.run/docs/en/v1/tutorials/blog) 7 | 8 | The steps taken to get us to this point (you don't have to do them!) were: 9 | 10 | ```sh 11 | npx create-remix --template remix-run/indie-stack remix 12 | ? TypeScript or JavaScript? TypeScript 13 | ? Do you want me to run `npm install`? No 14 | ``` 15 | 16 | For more information on the baseline Remix project, check their [README](https://github.com/remix-run/indie-stack) 17 | 18 | To get started, initialize the dependencies: 19 | 20 | ```sh 21 | npm install 22 | ``` 23 | 24 | And then set up your .env file. For local test deployments, the .example is good enough! 25 | 26 | ```sh 27 | cp .env.example .env # change values accordingly 28 | ``` 29 | 30 | > Notice that one of these variables is Prisma's `DATABASE_URL`. In this example, we will follow the Remix examples 31 | > to add Blog posts to the site, but we'll keep the user management that is already implemented in the example with 32 | > Prisma+SQLite. That's totally fine! We know that in real life projects have a history, and this example goes a long 33 | > way to show how ChiselStrike can be used to enhance an existing project. But if you want to convert the existing SQLite 34 | > code to ChiselStrike and send us a PR, we would obviously consider it! 35 | 36 | To initialize the ChiselStrike project, do this at the top level of your Remix directory: 37 | 38 | ```sh 39 | npx create-chiselstrike-app chiselstrike 40 | ``` 41 | 42 | First let's make sure that the Remix `run` command will also start ChiselStrike. Add this to the `scripts` session of your `package.json` file 43 | 44 | ```json 45 | "dev:chiselstrike": "cd chiselstrike; npm run dev", 46 | ``` 47 | 48 | Now Start the remix dev server: 49 | 50 | ```sh 51 | npm run dev 52 | ``` 53 | 54 | This starts your app in development mode, rebuilding assets on file changes. 55 | 56 | The (Prisma+SQLite) database seed script creates a new user with some data you can use to get started: 57 | 58 | - Email: `rachel@remix.run` 59 | - Password: `racheliscool` 60 | -------------------------------------------------------------------------------- /remix/app/db.server.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | declare global { 6 | var __db__: PrismaClient; 7 | } 8 | 9 | // this is needed because in development we don't want to restart 10 | // the server with every change, but we want to make sure we don't 11 | // create a new connection to the DB with every change either. 12 | // in production we'll have a single connection to the DB. 13 | if (process.env.NODE_ENV === "production") { 14 | prisma = new PrismaClient(); 15 | } else { 16 | if (!global.__db__) { 17 | global.__db__ = new PrismaClient(); 18 | } 19 | prisma = global.__db__; 20 | prisma.$connect(); 21 | } 22 | 23 | export { prisma }; 24 | -------------------------------------------------------------------------------- /remix/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { RemixBrowser } from "@remix-run/react"; 3 | import { hydrateRoot } from "react-dom/client"; 4 | 5 | function hydrate() { 6 | React.startTransition(() => { 7 | hydrateRoot( 8 | document, 9 | 10 | 11 | 12 | ); 13 | }); 14 | } 15 | 16 | if (window.requestIdleCallback) { 17 | window.requestIdleCallback(hydrate); 18 | } else { 19 | window.setTimeout(hydrate, 1); 20 | } 21 | -------------------------------------------------------------------------------- /remix/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { PassThrough } from "stream"; 2 | import { renderToPipeableStream } from "react-dom/server"; 3 | import { RemixServer } from "@remix-run/react"; 4 | import { Response } from "@remix-run/node"; 5 | import type { EntryContext, Headers } from "@remix-run/node"; 6 | import isbot from "isbot"; 7 | 8 | const ABORT_DELAY = 5000; 9 | 10 | export default function handleRequest( 11 | request: Request, 12 | responseStatusCode: number, 13 | responseHeaders: Headers, 14 | remixContext: EntryContext 15 | ) { 16 | const callbackName = isbot(request.headers.get("user-agent")) 17 | ? "onAllReady" 18 | : "onShellReady"; 19 | 20 | return new Promise((resolve, reject) => { 21 | let didError = false; 22 | 23 | const { pipe, abort } = renderToPipeableStream( 24 | , 25 | { 26 | [callbackName]() { 27 | let body = new PassThrough(); 28 | 29 | responseHeaders.set("Content-Type", "text/html"); 30 | 31 | resolve( 32 | new Response(body, { 33 | status: didError ? 500 : responseStatusCode, 34 | headers: responseHeaders, 35 | }) 36 | ); 37 | pipe(body); 38 | }, 39 | onShellError(err: unknown) { 40 | reject(err); 41 | }, 42 | onError(error: unknown) { 43 | didError = true; 44 | console.error(error); 45 | }, 46 | } 47 | ); 48 | setTimeout(abort, ABORT_DELAY); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /remix/app/models/note.server.ts: -------------------------------------------------------------------------------- 1 | import type { User, Note } from "@prisma/client"; 2 | 3 | import { prisma } from "~/db.server"; 4 | 5 | export type { Note } from "@prisma/client"; 6 | 7 | export function getNote({ 8 | id, 9 | userId, 10 | }: Pick & { 11 | userId: User["id"]; 12 | }) { 13 | return prisma.note.findFirst({ 14 | select: { id: true, body: true, title: true }, 15 | where: { id, userId }, 16 | }); 17 | } 18 | 19 | export function getNoteListItems({ userId }: { userId: User["id"] }) { 20 | return prisma.note.findMany({ 21 | where: { userId }, 22 | select: { id: true, title: true }, 23 | orderBy: { updatedAt: "desc" }, 24 | }); 25 | } 26 | 27 | export function createNote({ 28 | body, 29 | title, 30 | userId, 31 | }: Pick & { 32 | userId: User["id"]; 33 | }) { 34 | return prisma.note.create({ 35 | data: { 36 | title, 37 | body, 38 | user: { 39 | connect: { 40 | id: userId, 41 | }, 42 | }, 43 | }, 44 | }); 45 | } 46 | 47 | export function deleteNote({ 48 | id, 49 | userId, 50 | }: Pick & { userId: User["id"] }) { 51 | return prisma.note.deleteMany({ 52 | where: { id, userId }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /remix/app/models/post.server.ts: -------------------------------------------------------------------------------- 1 | // We import the ChiselStrike type directly and can reuse it in here. 2 | // It should also be possible to do the other way around: define the type 3 | // in the frontend, and get ChiselStrike to consume it. 4 | // 5 | // Progress on using user-define types is https://github.com/chiselstrike/chiselstrike/issues/1589. It's a great 6 | // starter issue for an OSS contributor! 7 | import type { Post } from "../../chiselstrike/Post"; 8 | export type { Post } from "../../chiselstrike/Post"; 9 | 10 | function chiselUrl(name: string): string { 11 | // Use environment variables to figure out where we are. Locally or deployed somehere? 12 | // By default we're local 13 | const chiselServer = process.env.CHISEL_SERVER ?? "http://localhost:8080"; 14 | // Which version? Versions are isolated backends that can be used in the same 15 | // chisel instance. You can do test databases, API versions, or what your heart 16 | // desires. By (local) default this is "dev", but when deployed it is customary 17 | // for this to be "main" (github's main branch) or a PR number. 18 | const chiselVersion = process.env.CHISEL_VERSION ?? "dev"; 19 | return `${chiselServer}/${chiselVersion}/${name}`; 20 | } 21 | 22 | // In practice you could use a higher level library here such as Axios, or 23 | // even hide everything behind tRPC. We are open coding fetches so it becomes 24 | // abundandtly obvious that those are really just HTTP endpoints! 25 | // 26 | // You can do anything you want with them, including going headless, microservices, etc. 27 | 28 | // The first function just gets all posts. No pagination, no filtering, though all of that 29 | // can be easily added! 30 | export async function getPosts(): Promise> { 31 | const url = chiselUrl("posts"); 32 | console.log(url); 33 | return fetch(url).then((response) => { 34 | return response.json().then((crud) => { 35 | return crud.results; 36 | }); 37 | }); 38 | } 39 | 40 | // The second function will just call the POST endpoint and create an object. 41 | export async function createPost(post) { 42 | const url = chiselUrl("posts"); 43 | return fetch(url, { 44 | method: "post", 45 | body: JSON.stringify(post), 46 | headers: { "Content-Type": "application/json" }, 47 | }); 48 | } 49 | 50 | // Last option is to get a post, given its ID. With this, we have everything we need! 51 | export async function getPost(id: string): Promise { 52 | const url = `${chiselUrl("posts")}/${id}`; 53 | return fetch(url).then((response) => { 54 | return response.json(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /remix/app/models/user.server.ts: -------------------------------------------------------------------------------- 1 | import type { Password, User } from "@prisma/client"; 2 | import bcrypt from "bcryptjs"; 3 | 4 | import { prisma } from "~/db.server"; 5 | 6 | export type { User } from "@prisma/client"; 7 | 8 | export async function getUserById(id: User["id"]) { 9 | return prisma.user.findUnique({ where: { id } }); 10 | } 11 | 12 | export async function getUserByEmail(email: User["email"]) { 13 | return prisma.user.findUnique({ where: { email } }); 14 | } 15 | 16 | export async function createUser(email: User["email"], password: string) { 17 | const hashedPassword = await bcrypt.hash(password, 10); 18 | 19 | return prisma.user.create({ 20 | data: { 21 | email, 22 | password: { 23 | create: { 24 | hash: hashedPassword, 25 | }, 26 | }, 27 | }, 28 | }); 29 | } 30 | 31 | export async function deleteUserByEmail(email: User["email"]) { 32 | return prisma.user.delete({ where: { email } }); 33 | } 34 | 35 | export async function verifyLogin( 36 | email: User["email"], 37 | password: Password["hash"] 38 | ) { 39 | const userWithPassword = await prisma.user.findUnique({ 40 | where: { email }, 41 | include: { 42 | password: true, 43 | }, 44 | }); 45 | 46 | if (!userWithPassword || !userWithPassword.password) { 47 | return null; 48 | } 49 | 50 | const isValid = await bcrypt.compare( 51 | password, 52 | userWithPassword.password.hash 53 | ); 54 | 55 | if (!isValid) { 56 | return null; 57 | } 58 | 59 | const { password: _password, ...userWithoutPassword } = userWithPassword; 60 | 61 | return userWithoutPassword; 62 | } 63 | -------------------------------------------------------------------------------- /remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction, LoaderArgs, MetaFunction } from "@remix-run/node"; 2 | import { json } from "@remix-run/node"; 3 | import { 4 | Links, 5 | LiveReload, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | } from "@remix-run/react"; 11 | 12 | import tailwindStylesheetUrl from "./styles/tailwind.css"; 13 | import { getUser } from "./session.server"; 14 | 15 | export const links: LinksFunction = () => { 16 | return [{ rel: "stylesheet", href: tailwindStylesheetUrl }]; 17 | }; 18 | 19 | export const meta: MetaFunction = () => ({ 20 | charset: "utf-8", 21 | title: "Remix Notes", 22 | viewport: "width=device-width,initial-scale=1", 23 | }); 24 | 25 | export async function loader({ request }: LoaderArgs) { 26 | return json({ 27 | user: await getUser(request), 28 | }); 29 | } 30 | 31 | export default function App() { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /remix/app/routes/healthcheck.tsx: -------------------------------------------------------------------------------- 1 | // learn more: https://fly.io/docs/reference/configuration/#services-http_checks 2 | import type { LoaderArgs } from "@remix-run/node"; 3 | 4 | import { prisma } from "~/db.server"; 5 | 6 | export async function loader({ request }: LoaderArgs) { 7 | const host = 8 | request.headers.get("X-Forwarded-Host") ?? request.headers.get("host"); 9 | 10 | try { 11 | const url = new URL("/", `http://${host}`); 12 | // if we can connect to the database and make a simple query 13 | // and make a HEAD request to ourselves, then we're good. 14 | await Promise.all([ 15 | prisma.user.count(), 16 | fetch(url.toString(), { method: "HEAD" }).then((r) => { 17 | if (!r.ok) return Promise.reject(r); 18 | }), 19 | ]); 20 | return new Response("OK"); 21 | } catch (error: unknown) { 22 | console.log("healthcheck ❌", { error }); 23 | return new Response("ERROR", { status: 500 }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /remix/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | 3 | import { useOptionalUser } from "~/utils"; 4 | 5 | export default function Index() { 6 | const user = useOptionalUser(); 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 | Sonic Youth On Stage 18 |
19 |
20 |
21 |

22 | 23 | Indie Stack 24 | 25 |

26 |

27 | Check the README.md file for instructions on how to get this 28 | project deployed. 29 |

30 |
31 | {user ? ( 32 | 36 | View Notes for {user.email} 37 | 38 | ) : ( 39 |
40 | 44 | Sign up 45 | 46 | 50 | Log In 51 | 52 |
53 | )} 54 |
55 | 56 | Remix 61 | 62 |
63 |
64 |
65 |
66 | 67 | Blog Posts 68 | 69 |
70 |
71 |
72 | {[ 73 | { 74 | src: "https://www.chiselstrike.com/_next/image?url=%2Flogos%2Fchiselstrike.svg&w=640&q=75", 75 | alt: "ChiselStrike", 76 | href: "https://chiselstrike.org", 77 | }, 78 | { 79 | src: "https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg", 80 | alt: "Fly.io", 81 | href: "https://fly.io", 82 | }, 83 | { 84 | src: "https://user-images.githubusercontent.com/1500684/157764395-137ec949-382c-43bd-a3c0-0cb8cb22e22d.svg", 85 | alt: "SQLite", 86 | href: "https://sqlite.org", 87 | }, 88 | { 89 | src: "https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg", 90 | alt: "Prisma", 91 | href: "https://prisma.io", 92 | }, 93 | { 94 | src: "https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg", 95 | alt: "Tailwind", 96 | href: "https://tailwindcss.com", 97 | }, 98 | { 99 | src: "https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg", 100 | alt: "Cypress", 101 | href: "https://www.cypress.io", 102 | }, 103 | { 104 | src: "https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg", 105 | alt: "MSW", 106 | href: "https://mswjs.io", 107 | }, 108 | { 109 | src: "https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg", 110 | alt: "Vitest", 111 | href: "https://vitest.dev", 112 | }, 113 | { 114 | src: "https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png", 115 | alt: "Testing Library", 116 | href: "https://testing-library.com", 117 | }, 118 | { 119 | src: "https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg", 120 | alt: "Prettier", 121 | href: "https://prettier.io", 122 | }, 123 | { 124 | src: "https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg", 125 | alt: "ESLint", 126 | href: "https://eslint.org", 127 | }, 128 | { 129 | src: "https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg", 130 | alt: "TypeScript", 131 | href: "https://typescriptlang.org", 132 | }, 133 | ].map((img) => ( 134 | 139 | {img.alt} 140 | 141 | ))} 142 |
143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /remix/app/routes/join.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node"; 2 | import { json, redirect } from "@remix-run/node"; 3 | import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; 4 | import * as React from "react"; 5 | 6 | import { getUserId, createUserSession } from "~/session.server"; 7 | 8 | import { createUser, getUserByEmail } from "~/models/user.server"; 9 | import { safeRedirect, validateEmail } from "~/utils"; 10 | 11 | export async function loader({ request }: LoaderArgs) { 12 | const userId = await getUserId(request); 13 | if (userId) return redirect("/"); 14 | return json({}); 15 | } 16 | 17 | export async function action({ request }: ActionArgs) { 18 | const formData = await request.formData(); 19 | const email = formData.get("email"); 20 | const password = formData.get("password"); 21 | const redirectTo = safeRedirect(formData.get("redirectTo"), "/"); 22 | 23 | if (!validateEmail(email)) { 24 | return json( 25 | { errors: { email: "Email is invalid", password: null } }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | if (typeof password !== "string" || password.length === 0) { 31 | return json( 32 | { errors: { email: null, password: "Password is required" } }, 33 | { status: 400 } 34 | ); 35 | } 36 | 37 | if (password.length < 8) { 38 | return json( 39 | { errors: { email: null, password: "Password is too short" } }, 40 | { status: 400 } 41 | ); 42 | } 43 | 44 | const existingUser = await getUserByEmail(email); 45 | if (existingUser) { 46 | return json( 47 | { 48 | errors: { 49 | email: "A user already exists with this email", 50 | password: null, 51 | }, 52 | }, 53 | { status: 400 } 54 | ); 55 | } 56 | 57 | const user = await createUser(email, password); 58 | 59 | return createUserSession({ 60 | request, 61 | userId: user.id, 62 | remember: false, 63 | redirectTo, 64 | }); 65 | } 66 | 67 | export const meta: MetaFunction = () => { 68 | return { 69 | title: "Sign Up", 70 | }; 71 | }; 72 | 73 | export default function Join() { 74 | const [searchParams] = useSearchParams(); 75 | const redirectTo = searchParams.get("redirectTo") ?? undefined; 76 | const actionData = useActionData(); 77 | const emailRef = React.useRef(null); 78 | const passwordRef = React.useRef(null); 79 | 80 | React.useEffect(() => { 81 | if (actionData?.errors?.email) { 82 | emailRef.current?.focus(); 83 | } else if (actionData?.errors?.password) { 84 | passwordRef.current?.focus(); 85 | } 86 | }, [actionData]); 87 | 88 | return ( 89 |
90 |
91 |
92 |
93 | 99 |
100 | 112 | {actionData?.errors?.email && ( 113 |
114 | {actionData.errors.email} 115 |
116 | )} 117 |
118 |
119 | 120 |
121 | 127 |
128 | 138 | {actionData?.errors?.password && ( 139 |
140 | {actionData.errors.password} 141 |
142 | )} 143 |
144 |
145 | 146 | 147 | 153 |
154 |
155 | Already have an account?{" "} 156 | 163 | Log in 164 | 165 |
166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /remix/app/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node"; 2 | import { json, redirect } from "@remix-run/node"; 3 | import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; 4 | import * as React from "react"; 5 | 6 | import { createUserSession, getUserId } from "~/session.server"; 7 | import { verifyLogin } from "~/models/user.server"; 8 | import { safeRedirect, validateEmail } from "~/utils"; 9 | 10 | export async function loader({ request }: LoaderArgs) { 11 | const userId = await getUserId(request); 12 | if (userId) return redirect("/"); 13 | return json({}); 14 | } 15 | 16 | export async function action({ request }: ActionArgs) { 17 | const formData = await request.formData(); 18 | const email = formData.get("email"); 19 | const password = formData.get("password"); 20 | const redirectTo = safeRedirect(formData.get("redirectTo"), "/notes"); 21 | const remember = formData.get("remember"); 22 | 23 | if (!validateEmail(email)) { 24 | return json( 25 | { errors: { email: "Email is invalid", password: null } }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | if (typeof password !== "string" || password.length === 0) { 31 | return json( 32 | { errors: { email: null, password: "Password is required" } }, 33 | { status: 400 } 34 | ); 35 | } 36 | 37 | if (password.length < 8) { 38 | return json( 39 | { errors: { email: null, password: "Password is too short" } }, 40 | { status: 400 } 41 | ); 42 | } 43 | 44 | const user = await verifyLogin(email, password); 45 | 46 | if (!user) { 47 | return json( 48 | { errors: { email: "Invalid email or password", password: null } }, 49 | { status: 400 } 50 | ); 51 | } 52 | 53 | return createUserSession({ 54 | request, 55 | userId: user.id, 56 | remember: remember === "on" ? true : false, 57 | redirectTo, 58 | }); 59 | } 60 | 61 | export const meta: MetaFunction = () => { 62 | return { 63 | title: "Login", 64 | }; 65 | }; 66 | 67 | export default function LoginPage() { 68 | const [searchParams] = useSearchParams(); 69 | const redirectTo = searchParams.get("redirectTo") || "/notes"; 70 | const actionData = useActionData(); 71 | const emailRef = React.useRef(null); 72 | const passwordRef = React.useRef(null); 73 | 74 | React.useEffect(() => { 75 | if (actionData?.errors?.email) { 76 | emailRef.current?.focus(); 77 | } else if (actionData?.errors?.password) { 78 | passwordRef.current?.focus(); 79 | } 80 | }, [actionData]); 81 | 82 | return ( 83 |
84 |
85 |
86 |
87 | 93 |
94 | 106 | {actionData?.errors?.email && ( 107 |
108 | {actionData.errors.email} 109 |
110 | )} 111 |
112 |
113 | 114 |
115 | 121 |
122 | 132 | {actionData?.errors?.password && ( 133 |
134 | {actionData.errors.password} 135 |
136 | )} 137 |
138 |
139 | 140 | 141 | 147 |
148 |
149 | 155 | 161 |
162 |
163 | Don't have an account?{" "} 164 | 171 | Sign up 172 | 173 |
174 |
175 |
176 |
177 |
178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /remix/app/routes/logout.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionArgs } from "@remix-run/node"; 2 | import { redirect } from "@remix-run/node"; 3 | 4 | import { logout } from "~/session.server"; 5 | 6 | export async function action({ request }: ActionArgs) { 7 | return logout(request); 8 | } 9 | 10 | export async function loader() { 11 | return redirect("/"); 12 | } 13 | -------------------------------------------------------------------------------- /remix/app/routes/notes.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderArgs } from "@remix-run/node"; 2 | import { json } from "@remix-run/node"; 3 | import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react"; 4 | 5 | import { requireUserId } from "~/session.server"; 6 | import { useUser } from "~/utils"; 7 | import { getNoteListItems } from "~/models/note.server"; 8 | 9 | export async function loader({ request }: LoaderArgs) { 10 | const userId = await requireUserId(request); 11 | const noteListItems = await getNoteListItems({ userId }); 12 | return json({ noteListItems }); 13 | } 14 | 15 | export default function NotesPage() { 16 | const data = useLoaderData(); 17 | const user = useUser(); 18 | 19 | return ( 20 |
21 |
22 |

23 | Notes 24 |

25 |

{user.email}

26 |
27 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | + New Note 40 | 41 | 42 |
43 | 44 | {data.noteListItems.length === 0 ? ( 45 |

No notes yet

46 | ) : ( 47 |
    48 | {data.noteListItems.map((note) => ( 49 |
  1. 50 | 52 | `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}` 53 | } 54 | to={note.id} 55 | > 56 | 📝 {note.title} 57 | 58 |
  2. 59 | ))} 60 |
61 | )} 62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /remix/app/routes/notes/$noteId.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionArgs, LoaderArgs } from "@remix-run/node"; 2 | import { json, redirect } from "@remix-run/node"; 3 | import { Form, useCatch, useLoaderData } from "@remix-run/react"; 4 | import invariant from "tiny-invariant"; 5 | 6 | import { deleteNote } from "~/models/note.server"; 7 | import { getNote } from "~/models/note.server"; 8 | import { requireUserId } from "~/session.server"; 9 | 10 | export async function loader({ request, params }: LoaderArgs) { 11 | const userId = await requireUserId(request); 12 | invariant(params.noteId, "noteId not found"); 13 | 14 | const note = await getNote({ userId, id: params.noteId }); 15 | if (!note) { 16 | throw new Response("Not Found", { status: 404 }); 17 | } 18 | return json({ note }); 19 | } 20 | 21 | export async function action({ request, params }: ActionArgs) { 22 | const userId = await requireUserId(request); 23 | invariant(params.noteId, "noteId not found"); 24 | 25 | await deleteNote({ userId, id: params.noteId }); 26 | 27 | return redirect("/notes"); 28 | } 29 | 30 | export default function NoteDetailsPage() { 31 | const data = useLoaderData(); 32 | 33 | return ( 34 |
35 |

{data.note.title}

36 |

{data.note.body}

37 |
38 |
39 | 45 |
46 |
47 | ); 48 | } 49 | 50 | export function ErrorBoundary({ error }: { error: Error }) { 51 | console.error(error); 52 | 53 | return
An unexpected error occurred: {error.message}
; 54 | } 55 | 56 | export function CatchBoundary() { 57 | const caught = useCatch(); 58 | 59 | if (caught.status === 404) { 60 | return
Note not found
; 61 | } 62 | 63 | throw new Error(`Unexpected caught response with status: ${caught.status}`); 64 | } 65 | -------------------------------------------------------------------------------- /remix/app/routes/notes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | 3 | export default function NoteIndexPage() { 4 | return ( 5 |

6 | No note selected. Select a note on the left, or{" "} 7 | 8 | create a new note. 9 | 10 |

11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /remix/app/routes/notes/new.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionArgs } from "@remix-run/node"; 2 | import { json, redirect } from "@remix-run/node"; 3 | import { Form, useActionData } from "@remix-run/react"; 4 | import * as React from "react"; 5 | 6 | import { createNote } from "~/models/note.server"; 7 | import { requireUserId } from "~/session.server"; 8 | 9 | export async function action({ request }: ActionArgs) { 10 | const userId = await requireUserId(request); 11 | 12 | const formData = await request.formData(); 13 | const title = formData.get("title"); 14 | const body = formData.get("body"); 15 | 16 | if (typeof title !== "string" || title.length === 0) { 17 | return json( 18 | { errors: { title: "Title is required", body: null } }, 19 | { status: 400 } 20 | ); 21 | } 22 | 23 | if (typeof body !== "string" || body.length === 0) { 24 | return json( 25 | { errors: { title: null, body: "Body is required" } }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | const note = await createNote({ title, body, userId }); 31 | 32 | return redirect(`/notes/${note.id}`); 33 | } 34 | 35 | export default function NewNotePage() { 36 | const actionData = useActionData(); 37 | const titleRef = React.useRef(null); 38 | const bodyRef = React.useRef(null); 39 | 40 | React.useEffect(() => { 41 | if (actionData?.errors?.title) { 42 | titleRef.current?.focus(); 43 | } else if (actionData?.errors?.body) { 44 | bodyRef.current?.focus(); 45 | } 46 | }, [actionData]); 47 | 48 | return ( 49 |
58 |
59 | 71 | {actionData?.errors?.title && ( 72 |
73 | {actionData.errors.title} 74 |
75 | )} 76 |
77 | 78 |
79 |