├── .gitignore ├── public └── favicon.ico ├── jsconfig.json ├── app ├── entry.client.jsx ├── routes │ ├── logout.jsx │ ├── offices │ │ └── $id.jsx │ ├── index.jsx │ ├── login.jsx │ └── profile.jsx ├── components │ ├── label.js │ ├── input.js │ ├── button.js │ └── errors.js ├── entry.server.jsx ├── services │ ├── axios.server.js │ └── auth.server.js └── root.jsx ├── remix.config.js ├── tailwind.config.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /.cache 4 | /build 5 | /public/build 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themsaid/ergodnc-remix/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./app/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/entry.client.jsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /app/routes/logout.jsx: -------------------------------------------------------------------------------- 1 | import {logout} from './../services/auth.server' 2 | import {redirect} from "remix"; 3 | 4 | export let action = async ({request}) => { 5 | return logout({request}); 6 | }; 7 | 8 | export let loader = async () => { 9 | return redirect("/"); 10 | }; -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev/config').AppConfig} 3 | */ 4 | module.exports = { 5 | appDirectory: "app", 6 | browserBuildDirectory: "public/build", 7 | publicPath: "/build/", 8 | serverBuildDirectory: "build", 9 | devServerPort: 8002 10 | }; 11 | -------------------------------------------------------------------------------- /app/components/label.js: -------------------------------------------------------------------------------- 1 | export default function Label({ children, className = '', ...props }) { 2 | return ( 3 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /app/components/input.js: -------------------------------------------------------------------------------- 1 | export default function Input({ disabled = false, className = '', ...props }) { 2 | return ( 3 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | purge: ['./app/**/*.{js,ts,jsx,tsx}'], 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 10 | }, 11 | }, 12 | }, 13 | variants: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } 18 | -------------------------------------------------------------------------------- /app/components/button.js: -------------------------------------------------------------------------------- 1 | export default function Button({ type = 'submit', className = '', ...props }) { 2 | return ( 3 | 36 | 37 | 38 | ); 39 | } -------------------------------------------------------------------------------- /app/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import {useLoaderData, Link} from "remix"; 2 | import axios from "./../services/axios.server" 3 | 4 | export let loader = async () => { 5 | let response = await axios.get('/offices'); 6 | 7 | return response.data.data; 8 | }; 9 | 10 | export let meta = () => { 11 | return { 12 | title: "ergodnc" 13 | }; 14 | }; 15 | 16 | export default function Index() { 17 | let offices = useLoaderData(); 18 | 19 | return ( 20 | <> 21 | {offices.map((office, index) => ( 22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |

{office.title}

30 | ${office.price_per_day / 100} per day 31 |
32 |

33 | {office.description} 34 |

35 | 36 | More details... 37 | 38 |
39 |
40 | ))} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/services/auth.server.js: -------------------------------------------------------------------------------- 1 | import {createCookieSessionStorage, redirect} from "remix"; 2 | import axios from "./axios.server" 3 | 4 | let storage = createCookieSessionStorage({ 5 | cookie: { 6 | name: "ergodnc_session", 7 | secure: process.env.NODE_ENV === "production", 8 | secrets: [process.env.SESSION_SECRET], 9 | sameSite: "lax", 10 | path: "/", 11 | maxAge: 60 * 60 * 24 * 30, 12 | httpOnly: true 13 | } 14 | }); 15 | 16 | export async function login({request, email, password}) { 17 | let response; 18 | let session = await storage.getSession( 19 | request.headers.get('Cookie') 20 | ); 21 | 22 | try { 23 | response = await axios.post("/login", {email, password}) 24 | } catch (error) { 25 | return {errors: Object.values(error.response.data.errors).flat()}; 26 | } 27 | 28 | session.set("userToken", response.data.token); 29 | 30 | return { 31 | redirector: redirect("/", { 32 | headers: { 33 | "Set-Cookie": await storage.commitSession(session) 34 | } 35 | }) 36 | }; 37 | }; 38 | 39 | export async function logout({request}) { 40 | const session = await storage.getSession( 41 | request.headers.get("Cookie") 42 | ); 43 | 44 | let token = session.get("userToken"); 45 | 46 | await axios.post("/logout", {}, { 47 | headers: { 48 | "Authorization": "Bearer " + token 49 | } 50 | }) 51 | 52 | return redirect("/login", { 53 | headers: { 54 | "Set-Cookie": await storage.destroySession(session) 55 | } 56 | }); 57 | }; 58 | 59 | export async function currentToken({request}) { 60 | const session = await storage.getSession( 61 | request.headers.get("Cookie") 62 | ); 63 | 64 | return session.get("userToken"); 65 | } 66 | 67 | export async function user({request}) { 68 | let response; 69 | let token = await currentToken({request}); 70 | 71 | try { 72 | response = await axios.get('/user', { 73 | headers: { 74 | "Authorization": "Bearer " + token 75 | } 76 | }) 77 | } catch (error) { 78 | return null; 79 | } 80 | 81 | return response.data.data; 82 | }; 83 | 84 | export async function requireGuest({request}) { 85 | if (await user({request})) { 86 | throw redirect("/"); 87 | } 88 | }; 89 | 90 | export async function requireAuth({request}) { 91 | let token = await currentToken({request}); 92 | 93 | if (!token) { 94 | throw redirect("/login"); 95 | } 96 | }; -------------------------------------------------------------------------------- /app/routes/login.jsx: -------------------------------------------------------------------------------- 1 | import {json, Link, Form, redirect, useActionData} from "remix"; 2 | import Errors from "./../components/errors" 3 | import Label from "./../components/label" 4 | import Input from "./../components/input" 5 | import Button from "./../components/button" 6 | import {login, requireGuest} from "./../services/auth.server" 7 | 8 | export let loader = async ({request, params}) => { 9 | await requireGuest({request}); 10 | 11 | return null; 12 | }; 13 | 14 | export let action = async ({request}) => { 15 | await requireGuest({request}); 16 | 17 | let formData = await request.formData(); 18 | let email = formData.get("email"); 19 | let password = formData.get("password"); 20 | 21 | let {errors, redirector} = await login({request, email, password}); 22 | 23 | return errors || redirector; 24 | }; 25 | 26 | export let meta = () => { 27 | return { 28 | title: "Sign In — ergodnc", 29 | }; 30 | }; 31 | 32 | export default function Login() { 33 | let errors = useActionData(); 34 | 35 | return ( 36 | <> 37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 | 52 |
53 | 54 |
55 | 56 | 57 | 65 |
66 | 67 |
68 | 69 | Forgot your password? 70 | 71 | 72 | 73 |
74 |
75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /app/routes/profile.jsx: -------------------------------------------------------------------------------- 1 | import {useLoaderData, Link} from "remix"; 2 | import {user, currentToken, requireAuth} from "./../services/auth.server" 3 | import axios from "./../services/axios.server" 4 | 5 | export let loader = async ({request, params}) => { 6 | await requireAuth({request}); 7 | 8 | let userToken = await currentToken({request}); 9 | 10 | let response = await axios.get('/reservations', { 11 | headers: { 12 | "Authorization": "Bearer " + userToken 13 | } 14 | }); 15 | 16 | return { 17 | reservations: response.data.data, 18 | user: await user({request}) 19 | }; 20 | }; 21 | 22 | export let meta = () => { 23 | return { 24 | title: "Profile — ergodnc", 25 | }; 26 | }; 27 | 28 | export default function Profile() { 29 | let {reservations, user} = useLoaderData(); 30 | 31 | return ( 32 | <> 33 |

34 | Hello {user.name}! 35 |

36 | 37 |
38 | Here is a list of your previous reservations! 39 |
40 | 41 | {reservations.map((reservation, index) => ( 42 |
43 |
44 | 45 |
46 | 47 | 48 |
49 |
50 |

{reservation.office.title}

51 | Total ${reservation.price / 100} 52 |
53 |

54 | 55 | From {reservation.start_date.split('T')[0]} To {reservation.end_date.split('T')[0]} 56 | 57 |

58 |

59 | {reservation.office.description} 60 |

61 | 62 | More details... 63 | 64 |
65 |
66 | ))} 67 | 68 | ); 69 | } -------------------------------------------------------------------------------- /app/root.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Link, 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | useCatch, useTransition, useLoaderData 10 | } from "remix"; 11 | 12 | import tailwindStyles from "./tailwind.css" 13 | import {user} from "./services/auth.server" 14 | 15 | export let loader = async ({request}) => { 16 | return await user({request}); 17 | }; 18 | 19 | export let links = () => { 20 | return [ 21 | {rel: "stylesheet", href: tailwindStyles}, 22 | ]; 23 | }; 24 | 25 | export default function App() { 26 | let user = useLoaderData(); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export function ErrorBoundary({error}) { 38 | console.error(error); 39 | return ( 40 | 41 | 42 |
43 |

There was an error

44 |

{error.message}

45 |
46 |

47 | Hey, developer, you should replace this with what you want your 48 | users to see. 49 |

50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | export function CatchBoundary() { 57 | let caught = useCatch(); 58 | 59 | let message; 60 | switch (caught.status) { 61 | case 401: 62 | message = ( 63 |

64 | Oops! Looks like you tried to visit a page that you do not have access 65 | to. 66 |

67 | ); 68 | break; 69 | case 404: 70 | message = ( 71 |

Oops! Looks like you tried to visit a page that does not exist.

72 | ); 73 | break; 74 | 75 | default: 76 | throw new Error(caught.data || caught.statusText); 77 | } 78 | 79 | return ( 80 | 81 | 82 |

83 | {caught.status}: {caught.statusText} 84 |

85 | {message} 86 |
87 |
88 | ); 89 | } 90 | 91 | function Document({children, title}) { 92 | return ( 93 | 94 | 95 | 96 | 97 | {title ? {title} : null} 98 | 99 | 100 | 101 | 102 | {children} 103 | 104 | 105 | {process.env.NODE_ENV === "development" && } 106 | 107 | 108 | ); 109 | } 110 | 111 | function Layout({children, user}) { 112 | let transition = useTransition(); 113 | return ( 114 | <> 115 |
116 |
117 |
118 | 119 |

120 | ergodnc 121 |

122 | 123 | 124 |
125 | {user 126 | ? 127 | <> 128 |
129 | 132 |
133 | 134 | 135 | Profile 136 | 137 | 138 | : 139 | <> 140 | 141 | Sign In 142 | 143 | 144 | Create Account 145 | 146 | 147 | } 148 |
149 |
150 |
151 |
152 | 153 |
154 | {transition.state == 'loading' 155 | ?
156 | {/*By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL*/} 157 | 158 | 159 | 163 | 167 | 168 | 169 | 173 | 177 | 178 | 179 | 183 | 187 | 188 | 189 |
190 | : children 191 | } 192 |
193 | 194 | ) 195 | } --------------------------------------------------------------------------------