├── .changeset ├── README.md ├── config.json └── lovely-pandas-design.md ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── README.md ├── cra.gif ├── devtools.png ├── next.gif ├── props.png ├── vite.gif └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── apps ├── cra │ ├── .env.local │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js ├── next │ ├── .env.local │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json └── remix │ ├── .dockerignore │ ├── .env.example │ ├── .eslintrc.js │ ├── .github │ └── workflows │ │ └── deploy.yml │ ├── .gitignore │ ├── .gitpod.Dockerfile │ ├── .gitpod.yml │ ├── .prettierignore │ ├── Dockerfile │ ├── README.md │ ├── app │ ├── db.server.ts │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── models │ │ ├── note.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 │ ├── session.server.ts │ ├── tailwind.css │ ├── utils.test.ts │ └── utils.ts │ ├── cypress.json │ ├── cypress │ ├── .eslintrc.js │ ├── e2e │ │ └── smoke.ts │ ├── fixtures │ │ └── example.json │ ├── plugins │ │ └── index.ts │ ├── support │ │ ├── commands.ts │ │ ├── create-user.ts │ │ ├── delete-user.ts │ │ └── index.ts │ └── tsconfig.json │ ├── fly.toml │ ├── mocks │ ├── README.md │ ├── index.js │ └── start.ts │ ├── package.json │ ├── postcss.config.js │ ├── prisma │ ├── migrations │ │ ├── 20220307190657_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── seed.ts │ ├── public │ └── favicon.ico │ ├── remix.config.js │ ├── remix.env.d.ts │ ├── remix.init │ ├── index.js │ ├── package-lock.json │ └── package.json │ ├── start.sh │ ├── tailwind.config.ts │ ├── test │ └── setup-test-env.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── package.json ├── packages └── click-to-react-component │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jsconfig.json │ ├── package.json │ └── src │ ├── ClickToComponent.js │ ├── ContextMenu.js │ ├── getDisplayNameFromReactInstance.js │ ├── getPathToSource.js │ ├── getPropsForInstance.js │ ├── getReactInstanceForElement.js │ ├── getReactInstancesForElement.js │ ├── getSourceForElement.js │ ├── getSourceForInstance.js │ ├── getUrl.js │ ├── index.js │ └── types.d.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["cra-app", "next-app", "remix-app"] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/lovely-pandas-design.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'click-to-react-component': patch 3 | --- 4 | 5 | Improve exports to support import fallback 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | ../packages/click-to-react-component/README.md -------------------------------------------------------------------------------- /.github/cra.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/.github/cra.gif -------------------------------------------------------------------------------- /.github/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/.github/devtools.png -------------------------------------------------------------------------------- /.github/next.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/.github/next.gif -------------------------------------------------------------------------------- /.github/props.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/.github/props.png -------------------------------------------------------------------------------- /.github/vite.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/.github/vite.gif -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | with: 18 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 19 | fetch-depth: 0 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Cache pnpm modules 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | restore-keys: | 32 | ${{ runner.os }}- 33 | 34 | - uses: pnpm/action-setup@v2 35 | with: 36 | run_install: true 37 | version: latest 38 | 39 | - name: Create Release Pull Request or Publish to npm 40 | id: changesets 41 | uses: changesets/action@v1 42 | with: 43 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 44 | publish: pnpm release 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .turbo 3 | .idea 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "js/ts.implicitProjectConfig.checkJs": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Clemmons 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 | -------------------------------------------------------------------------------- /apps/cra/.env.local: -------------------------------------------------------------------------------- 1 | REACT_APP_CTC_EDITOR=vscode-insiders -------------------------------------------------------------------------------- /apps/cra/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | # Do uncomment this before pushing to git repos, as app secrets might get public 17 | # .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /apps/cra/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /apps/cra/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /apps/cra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.1.1", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "5.0.1", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "click-to-react-component": "workspace:^1.1.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/cra/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/apps/cra/public/favicon.ico -------------------------------------------------------------------------------- /apps/cra/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/cra/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/apps/cra/public/logo192.png -------------------------------------------------------------------------------- /apps/cra/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/apps/cra/public/logo512.png -------------------------------------------------------------------------------- /apps/cra/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/cra/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/cra/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/cra/src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import './App.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 | logo 9 |

10 | Edit src/App.js and save to reload. 11 |

12 | 18 | Learn React 19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /apps/cra/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/cra/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /apps/cra/src/index.js: -------------------------------------------------------------------------------- 1 | import { ClickToComponent } from 'click-to-react-component'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import './index.css'; 5 | import App from './App'; 6 | import reportWebVitals from './reportWebVitals'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root')); 9 | root.render( 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /apps/cra/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cra/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /apps/cra/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /apps/next/.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CTC_EDITOR=cursor -------------------------------------------------------------------------------- /apps/next/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /apps/next/.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 | # Do uncomment this before pushing to git repos, as app secrets might get public 29 | # .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /apps/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /apps/next/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/next/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /apps/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "next-app", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "next": "*", 12 | "react": "*", 13 | "react-dom": "*" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "17.0.23", 17 | "@types/react": "18.0.3", 18 | "click-to-react-component": "workspace:^1.1.2", 19 | "eslint": "8.13.0", 20 | "eslint-config-next": "12.1.5", 21 | "typescript": "4.6.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/next/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ClickToComponent } from 'click-to-react-component' 2 | import type { AppProps } from 'next/app' 3 | 4 | import '../styles/globals.css' 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default MyApp 16 | -------------------------------------------------------------------------------- /apps/next/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /apps/next/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import Image from 'next/image' 4 | import styles from '../styles/Home.module.css' 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 | 69 | ) 70 | } 71 | 72 | export default Home 73 | -------------------------------------------------------------------------------- /apps/next/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ericclemmons/click-to-component/ba280452494d4163a97eeaa83be35349e3d27a65/apps/next/public/favicon.ico -------------------------------------------------------------------------------- /apps/next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /apps/next/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /apps/next/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 | -------------------------------------------------------------------------------- /apps/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": false, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/remix/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | .DS_Store 4 | .env 5 | /.cache 6 | /public/build 7 | /build 8 | -------------------------------------------------------------------------------- /apps/remix/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./data.db?connection_limit=1" 2 | SESSION_SECRET="super-duper-s3cret" 3 | -------------------------------------------------------------------------------- /apps/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 we have to explicitly 13 | // set the jest version. 14 | settings: { 15 | jest: { 16 | version: 27, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/remix/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - dev 7 | pull_request: {} 8 | 9 | jobs: 10 | lint: 11 | name: ⬣ ESLint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 🛑 Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | 17 | - name: ⬇️ Checkout repo 18 | uses: actions/checkout@v3 19 | 20 | - name: ⎔ Setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | 25 | - name: 📥 Download deps 26 | uses: bahmutov/npm-install@v1 27 | 28 | - name: 🔬 Lint 29 | run: npm run lint 30 | 31 | typecheck: 32 | name: ʦ TypeScript 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: 🛑 Cancel Previous Runs 36 | uses: styfle/cancel-workflow-action@0.9.1 37 | 38 | - name: ⬇️ Checkout repo 39 | uses: actions/checkout@v3 40 | 41 | - name: ⎔ Setup node 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: 16 45 | 46 | - name: 📥 Download deps 47 | uses: bahmutov/npm-install@v1 48 | 49 | - name: 🔎 Type check 50 | run: npm run typecheck --if-present 51 | 52 | vitest: 53 | name: ⚡ Vitest 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: 🛑 Cancel Previous Runs 57 | uses: styfle/cancel-workflow-action@0.9.1 58 | 59 | - name: ⬇️ Checkout repo 60 | uses: actions/checkout@v3 61 | 62 | - name: ⎔ Setup node 63 | uses: actions/setup-node@v3 64 | with: 65 | node-version: 16 66 | 67 | - name: 📥 Download deps 68 | uses: bahmutov/npm-install@v1 69 | 70 | - name: ⚡ Run vitest 71 | run: npm run test -- --coverage 72 | 73 | cypress: 74 | name: ⚫️ Cypress 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: 🛑 Cancel Previous Runs 78 | uses: styfle/cancel-workflow-action@0.9.1 79 | 80 | - name: ⬇️ Checkout repo 81 | uses: actions/checkout@v3 82 | 83 | - name: 🏄 Copy test env vars 84 | run: cp .env.example .env 85 | 86 | - name: ⎔ Setup node 87 | uses: actions/setup-node@v3 88 | with: 89 | node-version: 16 90 | 91 | - name: 📥 Download deps 92 | uses: bahmutov/npm-install@v1 93 | 94 | - name: 🛠 Setup Database 95 | run: npx prisma migrate reset --force 96 | 97 | - name: ⚙️ Build 98 | run: npm run build 99 | 100 | - name: 🌳 Cypress run 101 | uses: cypress-io/github-action@v3 102 | with: 103 | start: npm run start:mocks 104 | wait-on: "http://localhost:8811" 105 | env: 106 | PORT: "8811" 107 | 108 | build: 109 | name: 🐳 Build 110 | # only build/deploy main branch on pushes 111 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: 🛑 Cancel Previous Runs 115 | uses: styfle/cancel-workflow-action@0.9.1 116 | 117 | - name: ⬇️ Checkout repo 118 | uses: actions/checkout@v3 119 | 120 | - name: 👀 Read app name 121 | uses: SebRollen/toml-action@v1.0.0 122 | id: app_name 123 | with: 124 | file: "fly.toml" 125 | field: "app" 126 | 127 | - name: 🐳 Set up Docker Buildx 128 | uses: docker/setup-buildx-action@v1 129 | 130 | # Setup cache 131 | - name: ⚡️ Cache Docker layers 132 | uses: actions/cache@v2 133 | with: 134 | path: /tmp/.buildx-cache 135 | key: ${{ runner.os }}-buildx-${{ github.sha }} 136 | restore-keys: | 137 | ${{ runner.os }}-buildx- 138 | 139 | - name: 🔑 Fly Registry Auth 140 | uses: docker/login-action@v1 141 | with: 142 | registry: registry.fly.io 143 | username: x 144 | password: ${{ secrets.FLY_API_TOKEN }} 145 | 146 | - name: 🐳 Docker build 147 | uses: docker/build-push-action@v2 148 | with: 149 | context: . 150 | push: true 151 | tags: registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }} 152 | build-args: | 153 | COMMIT_SHA=${{ github.sha }} 154 | cache-from: type=local,src=/tmp/.buildx-cache 155 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new 156 | 157 | # This ugly bit is necessary if you don't want your cache to grow forever 158 | # till it hits GitHub's limit of 5GB. 159 | # Temp fix 160 | # https://github.com/docker/build-push-action/issues/252 161 | # https://github.com/moby/buildkit/issues/1896 162 | - name: 🚚 Move cache 163 | run: | 164 | rm -rf /tmp/.buildx-cache 165 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 166 | 167 | deploy: 168 | name: 🚀 Deploy 169 | runs-on: ubuntu-latest 170 | needs: [lint, typecheck, vitest, cypress, build] 171 | # only build/deploy main branch on pushes 172 | if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} 173 | 174 | steps: 175 | - name: 🛑 Cancel Previous Runs 176 | uses: styfle/cancel-workflow-action@0.9.1 177 | 178 | - name: ⬇️ Checkout repo 179 | uses: actions/checkout@v3 180 | 181 | - name: 👀 Read app name 182 | uses: SebRollen/toml-action@v1.0.0 183 | id: app_name 184 | with: 185 | file: "fly.toml" 186 | field: "app" 187 | 188 | - name: 🚀 Deploy Staging 189 | if: ${{ github.ref == 'refs/heads/dev' }} 190 | uses: superfly/flyctl-actions@1.3 191 | with: 192 | args: "deploy --app ${{ steps.app_name.outputs.value }}-staging --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" 193 | env: 194 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 195 | 196 | - name: 🚀 Deploy Production 197 | if: ${{ github.ref == 'refs/heads/main' }} 198 | uses: superfly/flyctl-actions@1.3 199 | with: 200 | args: "deploy --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" 201 | env: 202 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 203 | -------------------------------------------------------------------------------- /apps/remix/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /public/build 5 | .env 6 | 7 | /cypress/screenshots 8 | /cypress/videos 9 | /prisma/data.db 10 | /prisma/data.db-journal 11 | 12 | /app/styles/tailwind.css 13 | -------------------------------------------------------------------------------- /apps/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 | -------------------------------------------------------------------------------- /apps/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 | -------------------------------------------------------------------------------- /apps/remix/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /build 4 | /public/build 5 | .env 6 | 7 | /app/styles/tailwind.css 8 | -------------------------------------------------------------------------------- /apps/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 package-lock.json ./ 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 package-lock.json ./ 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 | ADD . . 58 | 59 | CMD ["npm", "start"] 60 | -------------------------------------------------------------------------------- /apps/remix/README.md: -------------------------------------------------------------------------------- 1 | # Remix Indie Stack 2 | 3 | ![The Remix Indie Stack](https://repository-images.githubusercontent.com/465928257/a241fa49-bd4d-485a-a2a5-5cb8e4ee0abf) 4 | 5 | Learn more about [Remix Stacks](https://remix.run/stacks). 6 | 7 | ``` 8 | npx create-remix --template remix-run/indie-stack 9 | ``` 10 | 11 | ## What's in the stack 12 | 13 | - [Fly app deployment](https://fly.io) with [Docker](https://www.docker.com/) 14 | - Production-ready [SQLite Database](https://sqlite.org) 15 | - Healthcheck endpoint for [Fly backups region fallbacks](https://fly.io/docs/reference/configuration/#services-http_checks) 16 | - [GitHub Actions](https://github.com/features/actions) for deploy on merge to production and staging environments 17 | - Email/Password Authentication with [cookie-based sessions](https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage) 18 | - Database ORM with [Prisma](https://prisma.io) 19 | - Styling with [Tailwind](https://tailwindcss.com/) 20 | - End-to-end testing with [Cypress](https://cypress.io) 21 | - Local third party request mocking with [MSW](https://mswjs.io) 22 | - Unit testing with [Vitest](https://vitest.dev) and [Testing Library](https://testing-library.com) 23 | - Code formatting with [Prettier](https://prettier.io) 24 | - Linting with [ESLint](https://eslint.org) 25 | - Static Types with [TypeScript](https://typescriptlang.org) 26 | 27 | Not a fan of bits of the stack? Fork it, change it, and use `npx create-remix --template your/repo`! Make it your own. 28 | 29 | ## Quickstart 30 | 31 | Click this button to create a [Gitpod](https://gitpod.io) workspace with the project set up and Fly pre-installed 32 | 33 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/) 34 | 35 | ## Development 36 | 37 | - Initial setup: _If you just generated this project, this step has been done for you._ 38 | 39 | ```sh 40 | npm run setup 41 | ``` 42 | 43 | - Start dev server: 44 | 45 | ```sh 46 | npm run dev 47 | ``` 48 | 49 | This starts your app in development mode, rebuilding assets on file changes. 50 | 51 | The database seed script creates a new user with some data you can use to get started: 52 | 53 | - Email: `rachel@remix.run` 54 | - Password: `racheliscool` 55 | 56 | ### Relevant code: 57 | 58 | This is a pretty simple note-taking app, but it's a good example of how you can build a full stack app with Prisma and Remix. The main functionality is creating users, logging in and out, and creating and deleting notes. 59 | 60 | - creating users, and logging in and out [./app/models/user.server.ts](./app/models/user.server.ts) 61 | - user sessions, and verifying them [./app/session.server.ts](./app/session.server.ts) 62 | - creating, and deleting notes [./app/models/note.server.ts](./app/models/note.server.ts) 63 | 64 | ## Deployment 65 | 66 | This Remix Stack comes with two GitHub Actions that handle automatically deploying your app to production and staging environments. 67 | 68 | Prior to your first deployment, you'll need to do a few things: 69 | 70 | - [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/) 71 | 72 | - Sign up and log in to Fly 73 | 74 | ```sh 75 | fly auth signup 76 | ``` 77 | 78 | > **Note:** If you have more than one Fly account, ensure that you are signed into the same account in the Fly CLI as you are in the browser. In your terminal, run `fly auth whoami` and ensure the email matches the Fly account signed into the browser. 79 | 80 | - Create two apps on Fly, one for staging and one for production: 81 | 82 | ```sh 83 | fly create remix-5961 84 | fly create remix-5961-staging 85 | ``` 86 | 87 | - Initialize Git. 88 | 89 | ```sh 90 | git init 91 | ``` 92 | 93 | - Create a new [GitHub Repository](https://repo.new), and then add it as the remote for your project. **Do not push your app yet!** 94 | 95 | ```sh 96 | git remote add origin 97 | ``` 98 | 99 | - Add a `FLY_API_TOKEN` to your GitHub repo. To do this, go to your user settings on Fly and create a new [token](https://web.fly.io/user/personal_access_tokens/new), then add it to [your repo secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) with the name `FLY_API_TOKEN`. 100 | 101 | - Add a `SESSION_SECRET` to your fly app secrets, to do this you can run the following commands: 102 | 103 | ```sh 104 | fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app remix-5961 105 | fly secrets set SESSION_SECRET=$(openssl rand -hex 32) --app remix-5961-staging 106 | ``` 107 | 108 | If you don't have openssl installed, you can also use [1password](https://1password.com/generate-password) to generate a random secret, just replace `$(openssl rand -hex 32)` with the generated secret. 109 | 110 | - Create a persistent volume for the sqlite database for both your staging and production environments. Run the following: 111 | 112 | ```sh 113 | fly volumes create data --size 1 --app remix-5961 114 | fly volumes create data --size 1 --app remix-5961-staging 115 | ``` 116 | 117 | Now that everything is set up you can commit and push your changes to your repo. Every commit to your `main` branch will trigger a deployment to your production environment, and every commit to your `dev` branch will trigger a deployment to your staging environment. 118 | 119 | ### Connecting to your database 120 | 121 | The sqlite database lives at `/data/sqlite.db` in your deployed application. You can connect to the live database by running `fly ssh console -C database-cli`. 122 | 123 | ### Getting Help with Deployment 124 | 125 | If you run into any issues deploying to Fly, make sure you've followed all of the steps above and if you have, then post as many details about your deployment (including your app name) to [the Fly support community](https://community.fly.io). They're normally pretty responsive over there and hopefully can help resolve any of your deployment issues and questions. 126 | 127 | ## GitHub Actions 128 | 129 | We use GitHub Actions for continuous integration and deployment. Anything that gets into the `main` branch will be deployed to production after running tests/build/etc. Anything in the `dev` branch will be deployed to staging. 130 | 131 | ## Testing 132 | 133 | ### Cypress 134 | 135 | We use Cypress for our End-to-End tests in this project. You'll find those in the `cypress` directory. As you make changes, add to an existing file or create a new file in the `cypress/e2e` directory to test your changes. 136 | 137 | We use [`@testing-library/cypress`](https://testing-library.com/cypress) for selecting elements on the page semantically. 138 | 139 | To run these tests in development, run `npm run test:e2e:dev` which will start the dev server for the app as well as the Cypress client. Make sure the database is running in docker as described above. 140 | 141 | We have a utility for testing authenticated features without having to go through the login flow: 142 | 143 | ```ts 144 | cy.login(); 145 | // you are now logged in as a new user 146 | ``` 147 | 148 | We also have a utility to auto-delete the user at the end of your test. Just make sure to add this in each test file: 149 | 150 | ```ts 151 | afterEach(() => { 152 | cy.cleanupUser(); 153 | }); 154 | ``` 155 | 156 | That way, we can keep your local db clean and keep your tests isolated from one another. 157 | 158 | ### Vitest 159 | 160 | For lower level tests of utilities and individual components, we use `vitest`. We have DOM-specific assertion helpers via [`@testing-library/jest-dom`](https://testing-library.com/jest-dom). 161 | 162 | ### Type Checking 163 | 164 | This project uses TypeScript. It's recommended to get TypeScript set up for your editor to get a really great in-editor experience with type checking and auto-complete. To run type checking across the whole project, run `npm run typecheck`. 165 | 166 | ### Linting 167 | 168 | This project uses ESLint for linting. That is configured in `.eslintrc.js`. 169 | 170 | ### Formatting 171 | 172 | We use [Prettier](https://prettier.io/) for auto-formatting in this project. It's recommended to install an editor plugin (like the [VSCode Prettier plugin](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) to get auto-formatting on save. There's also a `npm run format` script you can run to format all files in the project. 173 | -------------------------------------------------------------------------------- /apps/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 | -------------------------------------------------------------------------------- /apps/remix/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { hydrate } from "react-dom"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /apps/remix/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { EntryContext } from "@remix-run/node"; 2 | import { RemixServer } from "@remix-run/react"; 3 | import { renderToString } from "react-dom/server"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | const markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /apps/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 | where: { id, userId }, 15 | }); 16 | } 17 | 18 | export function getNoteListItems({ userId }: { userId: User["id"] }) { 19 | return prisma.note.findMany({ 20 | where: { userId }, 21 | select: { id: true, title: true }, 22 | orderBy: { updatedAt: "desc" }, 23 | }); 24 | } 25 | 26 | export function createNote({ 27 | body, 28 | title, 29 | userId, 30 | }: Pick & { 31 | userId: User["id"]; 32 | }) { 33 | return prisma.note.create({ 34 | data: { 35 | title, 36 | body, 37 | user: { 38 | connect: { 39 | id: userId, 40 | }, 41 | }, 42 | }, 43 | }); 44 | } 45 | 46 | export function deleteNote({ 47 | id, 48 | userId, 49 | }: Pick & { userId: User["id"] }) { 50 | return prisma.note.deleteMany({ 51 | where: { id, userId }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /apps/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 | -------------------------------------------------------------------------------- /apps/remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { cssBundleHref } from "@remix-run/css-bundle"; 2 | import type { LinksFunction, LoaderArgs } from "@remix-run/node"; 3 | import { ClickToComponent } from "click-to-react-component"; 4 | import { json } from "@remix-run/node"; 5 | import { 6 | Links, 7 | LiveReload, 8 | Meta, 9 | Outlet, 10 | Scripts, 11 | ScrollRestoration, 12 | useLoaderData, 13 | } from "@remix-run/react"; 14 | 15 | import { getUser } from "~/session.server"; 16 | import stylesheet from "~/tailwind.css"; 17 | 18 | export const links: LinksFunction = () => [ 19 | { rel: "stylesheet", href: stylesheet }, 20 | ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), 21 | ]; 22 | 23 | export const loader = async ({ request }: LoaderArgs) => { 24 | return json({ 25 | user: await getUser(request), 26 | projectDir: process.env.NODE_ENV === 'production' ? undefined : process.cwd(), 27 | }); 28 | }; 29 | 30 | export default function App() { 31 | const { projectDir } = useLoaderData(); 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | `${projectDir}/${path}`} /> 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/remix/app/routes/healthcheck.tsx: -------------------------------------------------------------------------------- 1 | // learn more: https://fly.io/docs/reference/configuration/#services-http_checks 2 | import type { LoaderFunction } from "@remix-run/node"; 3 | 4 | import { prisma } from "~/db.server"; 5 | 6 | export const loader: LoaderFunction = async ({ request }) => { 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 | -------------------------------------------------------------------------------- /apps/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 |
68 | {[ 69 | { 70 | src: "https://user-images.githubusercontent.com/1500684/157764397-ccd8ea10-b8aa-4772-a99b-35de937319e1.svg", 71 | alt: "Fly.io", 72 | href: "https://fly.io", 73 | }, 74 | { 75 | src: "https://user-images.githubusercontent.com/1500684/157764395-137ec949-382c-43bd-a3c0-0cb8cb22e22d.svg", 76 | alt: "SQLite", 77 | href: "https://sqlite.org", 78 | }, 79 | { 80 | src: "https://user-images.githubusercontent.com/1500684/157764484-ad64a21a-d7fb-47e3-8669-ec046da20c1f.svg", 81 | alt: "Prisma", 82 | href: "https://prisma.io", 83 | }, 84 | { 85 | src: "https://user-images.githubusercontent.com/1500684/157764276-a516a239-e377-4a20-b44a-0ac7b65c8c14.svg", 86 | alt: "Tailwind", 87 | href: "https://tailwindcss.com", 88 | }, 89 | { 90 | src: "https://user-images.githubusercontent.com/1500684/157764454-48ac8c71-a2a9-4b5e-b19c-edef8b8953d6.svg", 91 | alt: "Cypress", 92 | href: "https://www.cypress.io", 93 | }, 94 | { 95 | src: "https://user-images.githubusercontent.com/1500684/157772386-75444196-0604-4340-af28-53b236faa182.svg", 96 | alt: "MSW", 97 | href: "https://mswjs.io", 98 | }, 99 | { 100 | src: "https://user-images.githubusercontent.com/1500684/157772447-00fccdce-9d12-46a3-8bb4-fac612cdc949.svg", 101 | alt: "Vitest", 102 | href: "https://vitest.dev", 103 | }, 104 | { 105 | src: "https://user-images.githubusercontent.com/1500684/157772662-92b0dd3a-453f-4d18-b8be-9fa6efde52cf.png", 106 | alt: "Testing Library", 107 | href: "https://testing-library.com", 108 | }, 109 | { 110 | src: "https://user-images.githubusercontent.com/1500684/157772934-ce0a943d-e9d0-40f8-97f3-f464c0811643.svg", 111 | alt: "Prettier", 112 | href: "https://prettier.io", 113 | }, 114 | { 115 | src: "https://user-images.githubusercontent.com/1500684/157772990-3968ff7c-b551-4c55-a25c-046a32709a8e.svg", 116 | alt: "ESLint", 117 | href: "https://eslint.org", 118 | }, 119 | { 120 | src: "https://user-images.githubusercontent.com/1500684/157773063-20a0ed64-b9f8-4e0b-9d1e-0b65a3d4a6db.svg", 121 | alt: "TypeScript", 122 | href: "https://typescriptlang.org", 123 | }, 124 | ].map((img) => ( 125 | 130 | {img.alt} 131 | 132 | ))} 133 |
134 |
135 |
136 |
137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /apps/remix/app/routes/join.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunction, 3 | LoaderFunction, 4 | MetaFunction, 5 | } from "@remix-run/node"; 6 | import { json, redirect } from "@remix-run/node"; 7 | import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; 8 | import * as React from "react"; 9 | 10 | import { getUserId, createUserSession } from "~/session.server"; 11 | 12 | import { createUser, getUserByEmail } from "~/models/user.server"; 13 | import { safeRedirect, validateEmail } from "~/utils"; 14 | 15 | export const loader: LoaderFunction = async ({ request }) => { 16 | const userId = await getUserId(request); 17 | if (userId) return redirect("/"); 18 | return json({}); 19 | }; 20 | 21 | interface ActionData { 22 | errors: { 23 | email?: string; 24 | password?: string; 25 | }; 26 | } 27 | 28 | export const action: ActionFunction = async ({ request }) => { 29 | const formData = await request.formData(); 30 | const email = formData.get("email"); 31 | const password = formData.get("password"); 32 | const redirectTo = safeRedirect(formData.get("redirectTo"), "/"); 33 | 34 | if (!validateEmail(email)) { 35 | return json( 36 | { errors: { email: "Email is invalid" } }, 37 | { status: 400 } 38 | ); 39 | } 40 | 41 | if (typeof password !== "string") { 42 | return json( 43 | { errors: { password: "Password is required" } }, 44 | { status: 400 } 45 | ); 46 | } 47 | 48 | if (password.length < 8) { 49 | return json( 50 | { errors: { password: "Password is too short" } }, 51 | { status: 400 } 52 | ); 53 | } 54 | 55 | const existingUser = await getUserByEmail(email); 56 | if (existingUser) { 57 | return json( 58 | { errors: { email: "A user already exists with this email" } }, 59 | { status: 400 } 60 | ); 61 | } 62 | 63 | const user = await createUser(email, password); 64 | 65 | return createUserSession({ 66 | request, 67 | userId: user.id, 68 | remember: false, 69 | redirectTo, 70 | }); 71 | }; 72 | 73 | export const meta: MetaFunction = () => { 74 | return { 75 | title: "Sign Up", 76 | }; 77 | }; 78 | 79 | export default function Join() { 80 | const [searchParams] = useSearchParams(); 81 | const redirectTo = searchParams.get("redirectTo") ?? undefined; 82 | const actionData = useActionData() as ActionData; 83 | const emailRef = React.useRef(null); 84 | const passwordRef = React.useRef(null); 85 | 86 | React.useEffect(() => { 87 | if (actionData?.errors?.email) { 88 | emailRef.current?.focus(); 89 | } else if (actionData?.errors?.password) { 90 | passwordRef.current?.focus(); 91 | } 92 | }, [actionData]); 93 | 94 | return ( 95 |
96 |
97 |
98 |
99 | 105 |
106 | 118 | {actionData?.errors?.email && ( 119 |
120 | {actionData.errors.email} 121 |
122 | )} 123 |
124 |
125 | 126 |
127 | 133 |
134 | 144 | {actionData?.errors?.password && ( 145 |
146 | {actionData.errors.password} 147 |
148 | )} 149 |
150 |
151 | 152 | 153 | 159 |
160 |
161 | Already have an account?{" "} 162 | 169 | Log in 170 | 171 |
172 |
173 |
174 |
175 |
176 | ); 177 | } 178 | -------------------------------------------------------------------------------- /apps/remix/app/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunction, 3 | LoaderFunction, 4 | MetaFunction, 5 | } from "@remix-run/node"; 6 | import { json, redirect } from "@remix-run/node"; 7 | import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; 8 | import * as React from "react"; 9 | 10 | import { createUserSession, getUserId } from "~/session.server"; 11 | import { verifyLogin } from "~/models/user.server"; 12 | import { safeRedirect, validateEmail } from "~/utils"; 13 | 14 | export const loader: LoaderFunction = async ({ request }) => { 15 | const userId = await getUserId(request); 16 | if (userId) return redirect("/"); 17 | return json({}); 18 | }; 19 | 20 | interface ActionData { 21 | errors?: { 22 | email?: string; 23 | password?: string; 24 | }; 25 | } 26 | 27 | export const action: ActionFunction = async ({ request }) => { 28 | const formData = await request.formData(); 29 | const email = formData.get("email"); 30 | const password = formData.get("password"); 31 | const redirectTo = safeRedirect(formData.get("redirectTo"), "/notes"); 32 | const remember = formData.get("remember"); 33 | 34 | if (!validateEmail(email)) { 35 | return json( 36 | { errors: { email: "Email is invalid" } }, 37 | { status: 400 } 38 | ); 39 | } 40 | 41 | if (typeof password !== "string") { 42 | return json( 43 | { errors: { password: "Password is required" } }, 44 | { status: 400 } 45 | ); 46 | } 47 | 48 | if (password.length < 8) { 49 | return json( 50 | { errors: { password: "Password is too short" } }, 51 | { status: 400 } 52 | ); 53 | } 54 | 55 | const user = await verifyLogin(email, password); 56 | 57 | if (!user) { 58 | return json( 59 | { errors: { email: "Invalid email or password" } }, 60 | { status: 400 } 61 | ); 62 | } 63 | 64 | return createUserSession({ 65 | request, 66 | userId: user.id, 67 | remember: remember === "on" ? true : false, 68 | redirectTo, 69 | }); 70 | }; 71 | 72 | export const meta: MetaFunction = () => { 73 | return { 74 | title: "Login", 75 | }; 76 | }; 77 | 78 | export default function LoginPage() { 79 | const [searchParams] = useSearchParams(); 80 | const redirectTo = searchParams.get("redirectTo") || "/notes"; 81 | const actionData = useActionData() as ActionData; 82 | const emailRef = React.useRef(null); 83 | const passwordRef = React.useRef(null); 84 | 85 | React.useEffect(() => { 86 | if (actionData?.errors?.email) { 87 | emailRef.current?.focus(); 88 | } else if (actionData?.errors?.password) { 89 | passwordRef.current?.focus(); 90 | } 91 | }, [actionData]); 92 | 93 | return ( 94 |
95 |
96 |
97 |
98 | 104 |
105 | 117 | {actionData?.errors?.email && ( 118 |
119 | {actionData.errors.email} 120 |
121 | )} 122 |
123 |
124 | 125 |
126 | 132 |
133 | 143 | {actionData?.errors?.password && ( 144 |
145 | {actionData.errors.password} 146 |
147 | )} 148 |
149 |
150 | 151 | 152 | 158 |
159 |
160 | 166 | 172 |
173 |
174 | Don't have an account?{" "} 175 | 182 | Sign up 183 | 184 |
185 |
186 |
187 |
188 |
189 | ); 190 | } 191 | -------------------------------------------------------------------------------- /apps/remix/app/routes/logout.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction, LoaderFunction } from "@remix-run/node"; 2 | import { redirect } from "@remix-run/node"; 3 | 4 | import { logout } from "~/session.server"; 5 | 6 | export const action: ActionFunction = async ({ request }) => { 7 | return logout(request); 8 | }; 9 | 10 | export const loader: LoaderFunction = async () => { 11 | return redirect("/"); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/remix/app/routes/notes.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } 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 | type LoaderData = { 10 | noteListItems: Awaited>; 11 | }; 12 | 13 | export const loader: LoaderFunction = async ({ request }) => { 14 | const userId = await requireUserId(request); 15 | const noteListItems = await getNoteListItems({ userId }); 16 | return json({ noteListItems }); 17 | }; 18 | 19 | export default function NotesPage() { 20 | const data = useLoaderData() as LoaderData; 21 | const user = useUser(); 22 | 23 | return ( 24 |
25 |
26 |

27 | Notes 28 |

29 |

{user.email}

30 |
31 | 37 |
38 |
39 | 40 |
41 |
42 | 43 | + New Note 44 | 45 | 46 |
47 | 48 | {data.noteListItems.length === 0 ? ( 49 |

No notes yet

50 | ) : ( 51 |
    52 | {data.noteListItems.map((note) => ( 53 |
  1. 54 | 56 | `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}` 57 | } 58 | to={note.id} 59 | > 60 | 📝 {note.title} 61 | 62 |
  2. 63 | ))} 64 |
65 | )} 66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /apps/remix/app/routes/notes/$noteId.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction, LoaderFunction } 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 type { Note } from "~/models/note.server"; 7 | import { deleteNote } from "~/models/note.server"; 8 | import { getNote } from "~/models/note.server"; 9 | import { requireUserId } from "~/session.server"; 10 | 11 | type LoaderData = { 12 | note: Note; 13 | }; 14 | 15 | export const loader: LoaderFunction = async ({ request, params }) => { 16 | const userId = await requireUserId(request); 17 | invariant(params.noteId, "noteId not found"); 18 | 19 | const note = await getNote({ userId, id: params.noteId }); 20 | if (!note) { 21 | throw new Response("Not Found", { status: 404 }); 22 | } 23 | return json({ note }); 24 | }; 25 | 26 | export const action: ActionFunction = async ({ request, params }) => { 27 | const userId = await requireUserId(request); 28 | invariant(params.noteId, "noteId not found"); 29 | 30 | await deleteNote({ userId, id: params.noteId }); 31 | 32 | return redirect("/notes"); 33 | }; 34 | 35 | export default function NoteDetailsPage() { 36 | const data = useLoaderData() as LoaderData; 37 | 38 | return ( 39 |
40 |

{data.note.title}

41 |

{data.note.body}

42 |
43 |
44 | 50 |
51 |
52 | ); 53 | } 54 | 55 | export function ErrorBoundary({ error }: { error: Error }) { 56 | console.error(error); 57 | 58 | return
An unexpected error occurred: {error.message}
; 59 | } 60 | 61 | export function CatchBoundary() { 62 | const caught = useCatch(); 63 | 64 | if (caught.status === 404) { 65 | return
Note not found
; 66 | } 67 | 68 | throw new Error(`Unexpected caught response with status: ${caught.status}`); 69 | } 70 | -------------------------------------------------------------------------------- /apps/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 | -------------------------------------------------------------------------------- /apps/remix/app/routes/notes/new.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction } 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 | type ActionData = { 10 | errors?: { 11 | title?: string; 12 | body?: string; 13 | }; 14 | }; 15 | 16 | export const action: ActionFunction = async ({ request }) => { 17 | const userId = await requireUserId(request); 18 | 19 | const formData = await request.formData(); 20 | const title = formData.get("title"); 21 | const body = formData.get("body"); 22 | 23 | if (typeof title !== "string" || title.length === 0) { 24 | return json( 25 | { errors: { title: "Title is required" } }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | if (typeof body !== "string" || body.length === 0) { 31 | return json( 32 | { errors: { body: "Body is required" } }, 33 | { status: 400 } 34 | ); 35 | } 36 | 37 | const note = await createNote({ title, body, userId }); 38 | 39 | return redirect(`/notes/${note.id}`); 40 | }; 41 | 42 | export default function NewNotePage() { 43 | const actionData = useActionData() as ActionData; 44 | const titleRef = React.useRef(null); 45 | const bodyRef = React.useRef(null); 46 | 47 | React.useEffect(() => { 48 | if (actionData?.errors?.title) { 49 | titleRef.current?.focus(); 50 | } else if (actionData?.errors?.body) { 51 | bodyRef.current?.focus(); 52 | } 53 | }, [actionData]); 54 | 55 | return ( 56 |
65 |
66 | 78 | {actionData?.errors?.title && ( 79 |
80 | {actionData.errors.title} 81 |
82 | )} 83 |
84 | 85 |
86 |