├── .eslintrc ├── .gitignore ├── Happy-Days-Guide-Updated-9:15.docx ├── Happy-Days-Guide-Updated-9:15.pdf ├── README.md ├── app ├── components │ ├── entry-form.tsx │ └── navigation.tsx ├── entry.client.tsx ├── entry.server.tsx ├── root.tsx ├── routes │ ├── api │ │ └── auth │ │ │ ├── login.tsx │ │ │ └── logout.tsx │ ├── cancel.tsx │ ├── entries │ │ ├── $id │ │ │ ├── edit.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── new.tsx │ ├── index.tsx │ ├── login.tsx │ └── success.tsx ├── types.ts └── utils │ ├── cookies.ts │ ├── supabase.ts │ └── withAuth.ts ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── server.js ├── styles └── app.css ├── supabase ├── config.toml └── functions │ ├── create-stripe-checkout │ └── index.ts │ ├── create-stripe-customer │ └── index.ts │ ├── stripe-customer-portal │ └── index.ts │ └── stripe-webhooks │ └── index.ts ├── tailwind.config.js ├── tsconfig.json └── wrangler.toml /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /dist 6 | /public/build 7 | /.mf 8 | .env 9 | 10 | # Supabase 11 | **/supabase/.branches 12 | **/supabase/.temp 13 | 14 | # Tailwind 15 | app/styles/app.css 16 | -------------------------------------------------------------------------------- /Happy-Days-Guide-Updated-9:15.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dijonmusters/happy-days/281cc5aa5e95fb06d6a94a3fe27637e0b1d89ce0/Happy-Days-Guide-Updated-9:15.docx -------------------------------------------------------------------------------- /Happy-Days-Guide-Updated-9:15.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dijonmusters/happy-days/281cc5aa5e95fb06d6a94a3fe27637e0b1d89ce0/Happy-Days-Guide-Updated-9:15.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | You will be running two processes during development: 8 | 9 | - The Miniflare server (miniflare is a local environment for Cloudflare Workers) 10 | - The Remix development server 11 | 12 | Both are started with one command: 13 | 14 | ```sh 15 | npm run dev 16 | ``` 17 | 18 | Open up [http://127.0.0.1:8787](http://127.0.0.1:8787) and you should be ready to go! 19 | 20 | If you want to check the production build, you can stop the dev server and run following commands: 21 | 22 | ```sh 23 | npm run build 24 | npm start 25 | ``` 26 | 27 | Then refresh the same URL in your browser (no live reload for production builds). 28 | 29 | ## Deployment 30 | 31 | Use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler) to build and deploy your application to Cloudflare Workers. If you don't have it yet, follow [the installation guide](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to get it setup. Be sure to [authenticate the CLI](https://developers.cloudflare.com/workers/cli-wrangler/authentication) as well. 32 | 33 | If you don't already have an account, then [create a cloudflare account here](https://dash.cloudflare.com/sign-up) and after verifying your email address with Cloudflare, go to your dashboard and set up your free custom Cloudflare Workers subdomain. 34 | 35 | Once that's done, you should be able to deploy your app: 36 | 37 | ```sh 38 | npm run deploy 39 | ``` 40 | -------------------------------------------------------------------------------- /app/components/entry-form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | redirect, 3 | unstable_parseMultipartFormData, 4 | unstable_composeUploadHandlers, 5 | unstable_createMemoryUploadHandler, 6 | } from "@remix-run/cloudflare"; 7 | import { Form, useLocation, useNavigate } from "@remix-run/react"; 8 | import { Entry } from "~/types"; 9 | import supabase from "~/utils/supabase"; 10 | import withAuth from "~/utils/withAuth"; 11 | 12 | type EntryFormProps = { 13 | entry?: Entry; 14 | }; 15 | 16 | const asyncIterableToStream = (asyncIterable: AsyncIterable) => { 17 | console.log("inside stream"); 18 | 19 | try { 20 | return new ReadableStream({ 21 | async pull(controller) { 22 | for await (const entry of asyncIterable) { 23 | controller.enqueue(entry); 24 | } 25 | controller.close(); 26 | }, 27 | }); 28 | } catch (e) { 29 | console.log(e); 30 | throw new Error(e); 31 | } 32 | }; 33 | 34 | export const action = withAuth(async ({ supabaseClient, request, user }) => { 35 | const uploadHandler = unstable_composeUploadHandlers(async (file) => { 36 | console.log("start of upload handler"); 37 | if (file.name !== "files") { 38 | console.log("not a file"); 39 | return undefined; 40 | } 41 | 42 | console.log("is a file"); 43 | 44 | const stream = asyncIterableToStream(file.data); 45 | 46 | console.log("stream creation successful"); 47 | 48 | const filepath = `${user!.id}/${file.filename}`; 49 | const { data, error } = await supabaseClient.storage 50 | .from("assets") 51 | .upload(filepath, stream, { 52 | contentType: file.contentType, 53 | upsert: true, 54 | }); 55 | 56 | if (error) { 57 | throw error; 58 | } 59 | 60 | console.log("end of upload handler"); 61 | 62 | return filepath; 63 | }, unstable_createMemoryUploadHandler()); 64 | 65 | // const uploadHandler = unstable_createMemoryUploadHandler({ 66 | // maxPartSize: 20_000_000, 67 | // }); 68 | 69 | const formData = await unstable_parseMultipartFormData( 70 | request, 71 | uploadHandler 72 | ); 73 | 74 | console.log("upload handler successful"); 75 | 76 | const id = formData.get("id")?.toString(); 77 | const date = formData.get("date")?.toString(); 78 | const title = formData.get("title")?.toString(); 79 | const content = formData.get("content")?.toString(); 80 | const filePaths = formData.getAll("files").map((file) => file.toString()); 81 | 82 | const { data: entryData, error: entryError } = await supabaseClient 83 | .from("entries") 84 | .upsert({ 85 | id, 86 | date, 87 | title, 88 | content, 89 | asset_urls: filePaths, 90 | }) 91 | .single(); 92 | 93 | console.log("supabase upsert successful"); 94 | 95 | if (entryError) { 96 | throw entryError; 97 | } 98 | 99 | return redirect(`/entries/${entryData.id}`); 100 | }); 101 | 102 | const EntryForm = ({ entry }: EntryFormProps) => { 103 | const navigate = useNavigate(); 104 | const addEntry = async (e) => { 105 | e.preventDefault(); 106 | 107 | const formData = new FormData(e.target); 108 | 109 | const id = formData.get("id")?.toString(); 110 | const date = formData.get("date")?.toString(); 111 | const title = formData.get("title")?.toString(); 112 | const content = formData.get("content")?.toString(); 113 | const files = e.target.files.files; 114 | 115 | console.log({ files }); 116 | 117 | const userId = supabase.auth.user()?.id; 118 | 119 | const filepath = `${userId}/${files[0].filename}`; 120 | 121 | const { data, error } = await supabase.storage 122 | .from("assets") 123 | .upload(filepath, files[0], { 124 | upsert: true, 125 | }); 126 | 127 | console.log({ error }); 128 | 129 | const { data: entryData, error: entryError } = await supabase 130 | .from("entries") 131 | .upsert({ 132 | id, 133 | date, 134 | title, 135 | content, 136 | asset_urls: [filepath], 137 | }) 138 | .single(); 139 | 140 | console.log({ entryError }); 141 | 142 | navigate(`/entries/${entryData?.id}`); 143 | }; 144 | 145 | return ( 146 |
150 | {entry && } 151 |
152 | 159 | 165 |
166 |
167 | 174 | 180 |
181 |
182 | 188 | 194 |
195 |
196 | 202 | 208 |
209 | 215 |
216 | ); 217 | }; 218 | 219 | export default EntryForm; 220 | -------------------------------------------------------------------------------- /app/components/navigation.tsx: -------------------------------------------------------------------------------- 1 | import supabase from "~/utils/supabase"; 2 | import { NavLink } from "@remix-run/react"; 3 | 4 | const Navigation = () => { 5 | return ( 6 | 35 | ); 36 | }; 37 | 38 | export default Navigation; 39 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { hydrate } from "react-dom"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { EntryContext } from "@remix-run/cloudflare"; 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 | let 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 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { json, MetaFunction } from "@remix-run/cloudflare"; 2 | import { 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | useFetcher, 10 | useLoaderData, 11 | } from "@remix-run/react"; 12 | import { useEffect, useState } from "react"; 13 | import styles from "./styles/app.css"; 14 | import supabase from "./utils/supabase"; 15 | 16 | export function links() { 17 | return [{ rel: "stylesheet", href: styles }]; 18 | } 19 | 20 | export const meta: MetaFunction = () => ({ 21 | charset: "utf-8", 22 | title: "New Remix App", 23 | viewport: "width=device-width,initial-scale=1", 24 | }); 25 | 26 | export const loader = () => { 27 | return json({ env: { SUPABASE_URL, SUPABASE_ANON_KEY, STRIPE_PUBLIC_KEY } }); 28 | }; 29 | 30 | export default function App() { 31 | const { env } = useLoaderData(); 32 | const fetcher = useFetcher(); 33 | 34 | useEffect(() => { 35 | const { data: listener } = supabase.auth.onAuthStateChange( 36 | (event, session) => { 37 | if (event === "SIGNED_IN" && session?.access_token) { 38 | fetcher.submit( 39 | { 40 | accessToken: session.access_token, 41 | }, 42 | { 43 | method: "post", 44 | action: "/api/auth/login", 45 | } 46 | ); 47 | } 48 | if (event === "SIGNED_OUT") { 49 | fetcher.submit(null, { 50 | method: "post", 51 | action: "/api/auth/logout", 52 | }); 53 | } 54 | } 55 | ); 56 | 57 | return () => { 58 | listener?.unsubscribe(); 59 | }; 60 | }, []); 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |