├── .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 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/app/entry.server.jsx:
--------------------------------------------------------------------------------
1 | import { renderToString } from "react-dom/server";
2 | import { RemixServer } from "remix";
3 |
4 | export default function handleRequest(
5 | request,
6 | responseStatusCode,
7 | responseHeaders,
8 | remixContext
9 | ) {
10 | let markup = renderToString(
11 |
12 | );
13 |
14 | responseHeaders.set("Content-Type", "text/html");
15 |
16 | return new Response("" + markup, {
17 | status: responseStatusCode,
18 | headers: responseHeaders,
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/app/components/errors.js:
--------------------------------------------------------------------------------
1 | export default function Label({ errors = [], ...props }) {
2 | return (
3 | <>
4 | {errors.length > 0 && (
5 |
6 |
7 | Whoops! Something went wrong.
8 |
9 |
10 |
11 | {errors.map(error => (
12 | - {error}
13 | ))}
14 |
15 |
16 | )}
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/app/services/axios.server.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import {redirect} from "remix";
3 |
4 | let client = axios.create({
5 | baseURL: process.env.API_HOST,
6 | headers: {
7 | 'X-Requested-With': 'XMLHttpRequest',
8 | "Content-Type": "application/json"
9 | },
10 | });
11 |
12 | client.interceptors.response.use(
13 | (response) => response,
14 | (error) => {
15 | if (!error.response) {
16 | return Promise.reject(error)
17 | }
18 |
19 | if (error.response.status === 401) {
20 | throw redirect("/login");
21 | }
22 |
23 | return Promise.reject(error)
24 | },
25 | )
26 |
27 | export default client;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "remix-app-template-js",
4 | "description": "",
5 | "license": "",
6 | "scripts": {
7 | "build": "npm run build:css && remix build",
8 | "build:css": "tailwindcss -o ./app/tailwind.css",
9 | "dev": "concurrently \"npm run dev:css\" \"node -r dotenv/config node_modules/.bin/remix dev\"",
10 | "dev:css": "tailwindcss -o ./app/tailwind.css --watch",
11 | "postinstall": "remix setup node",
12 | "start": "remix-serve build"
13 | },
14 | "dependencies": {
15 | "@remix-run/react": "^1.0.6",
16 | "@remix-run/serve": "^1.0.6",
17 | "axios": "^0.24.0",
18 | "dotenv": "^10.0.0",
19 | "react": "^17.0.2",
20 | "react-dom": "^17.0.2",
21 | "remix": "^1.0.6"
22 | },
23 | "devDependencies": {
24 | "@remix-run/dev": "^1.0.6",
25 | "autoprefixer": "^10.4.0",
26 | "concurrently": "^6.4.0",
27 | "postcss": "^8.4.4",
28 | "tailwindcss": "^2.2.19"
29 | },
30 | "engines": {
31 | "node": ">=14"
32 | },
33 | "sideEffects": false
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Remix!
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Development
6 |
7 | From your terminal:
8 |
9 | ```sh
10 | npm run dev
11 | ```
12 |
13 | This starts your app in development mode, rebuilding assets on file changes.
14 |
15 | ## Deployment
16 |
17 | First, build your app for production:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | Then run the app in production mode:
24 |
25 | ```sh
26 | npm start
27 | ```
28 |
29 | Now you'll need to pick a host to deploy it to.
30 |
31 | ### DIY
32 |
33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
34 |
35 | Make sure to deploy the output of `remix build`
36 |
37 | - `build/`
38 | - `public/build/`
39 |
40 | ### Using a Template
41 |
42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
43 |
44 | ```sh
45 | cd ..
46 | # create a new project, and pick a pre-configured host
47 | npx create-remix@latest
48 | cd my-new-remix-app
49 | # remove the new project's app (not the old one!)
50 | rm -rf app
51 | # copy your app over
52 | cp -R ../my-old-remix-app/app app
53 | ```
54 |
--------------------------------------------------------------------------------
/app/routes/offices/$id.jsx:
--------------------------------------------------------------------------------
1 | import {useCatch, Link, json, useLoaderData} from "remix";
2 | import Button from '../../components/button'
3 | import axios from "./../../services/axios.server"
4 |
5 | export let loader = async ({params}) => {
6 | let response = await axios.get('/offices/' + params.id);
7 |
8 | return response.data.data;
9 | };
10 |
11 | export let meta = ({office}) => {
12 | return {
13 | title: office ? `${office.name} — ergodnc` : "ergodnc",
14 | };
15 | };
16 |
17 |
18 | export default function Office() {
19 | let office = useLoaderData();
20 |
21 | return (
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 |
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 |
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 |
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 |
189 |
190 | : children
191 | }
192 |
193 | >
194 | )
195 | }
--------------------------------------------------------------------------------