Skip to content

Commit

Permalink
react router to remix spa
Browse files Browse the repository at this point in the history
  • Loading branch information
stevan-borus committed Jun 13, 2024
1 parent afc421a commit 7666f5b
Show file tree
Hide file tree
Showing 43 changed files with 4,428 additions and 531 deletions.
81 changes: 71 additions & 10 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,78 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},
ignorePatterns: ['!**/.server', '!**/.client'],

overrides: [
// React
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: ['react', 'jsx-a11y'],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
settings: {
react: {
version: 'detect',
},
formComponents: ['Form'],
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
'import/resolver': {
typescript: {},
},
},
},

// Typescript
{
files: ['**/*.{ts,tsx}'],
plugins: ['@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser',
settings: {
'import/internal-regex': '^~/',
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
},

// Node
{
files: ['.eslintrc.cjs'],
env: {
node: true,
},
},
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'prefer-const': 0,
},
};
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?

/.cache
/build
/public/build
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
34 changes: 34 additions & 0 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/

import { RemixBrowser } from '@remix-run/react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';

async function enableMocking() {
if (import.meta.env.VITE_NODE_ENV !== 'development') {
return;
}

const { worker } = await import('./mocks/browser');

// `worker.start()` returns a Promise that resolves
// once the Service Worker is up and ready to intercept requests.
return worker.start({
onUnhandledRequest: 'bypass',
});
}

enableMocking().then(() => {
startTransition(() => {
hydrateRoot(
document.querySelector('#app'),
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
});
24 changes: 24 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import fs from 'node:fs';
import path from 'node:path';

import type { EntryContext } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { renderToString } from 'react-dom/server';

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const shellHtml = fs.readFileSync(path.join(process.cwd(), 'app/index.html')).toString();

const appHtml = renderToString(<RemixServer context={remixContext} url={request.url} />);

const html = shellHtml.replace('<!-- Remix SPA -->', appHtml);

return new Response(html, {
headers: { 'Content-Type': 'text/html' },
status: responseStatusCode,
});
}
25 changes: 12 additions & 13 deletions index.html → app/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
</head>
<body>
<div id="app"><!-- Remix SPA --></div>
</body>
</html>
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions src/model/note.ts → app/model/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export const noteSchema = z.object({
});

export const notesSchema = z.array(noteSchema);
// export const notesSchema = z.array(noteSchema).promise();

export type NoteType = z.infer<typeof noteSchema>;
File renamed without changes.
69 changes: 69 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Links,
Meta,
Outlet,
Scripts,
isRouteErrorResponse,
useNavigate,
useRouteError,
} from '@remix-run/react';
import { Button } from './components/ui/button';
import '@/tailwind.css';

export default function Component() {
return (
<>
<Outlet />
<Meta />
<Links />
<Scripts />
</>
);
}

export function HydrateFallback() {
return (
<>
<p>Loading...</p>
<Scripts />
</>
);
}

export function ErrorBoundary() {
let error = useRouteError();

const navigate = useNavigate();

let errorMessage = '';

if (isRouteErrorResponse(error)) {
errorMessage = error.statusText;
} else if (error instanceof Error) {
errorMessage = error.message;
}

return (
<html>
<head>
<title>Oh no!</title>
<Meta />
<Links />
</head>
<body>
<div className='flex flex-col items-center justify-center gap-2 py-8'>
<h1>Oops!</h1>

<p>Sorry, an unexpected error has occurred</p>

<p>
<i>{errorMessage}</i>
</p>

<Button onClick={() => navigate('/')}>Reload page</Button>
</div>
<Scripts />
</body>
</html>
);
}
20 changes: 7 additions & 13 deletions src/routes/Auth.tsx → app/routes/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
Form,
LoaderFunctionArgs,
redirect,
useActionData,
useLocation,
useNavigation,
} from 'react-router-dom';
import { Form, redirect, useActionData, useLocation, useNavigation } from '@remix-run/react';
import { LoaderFunctionArgs } from '@remix-run/node';
import { z } from 'zod';

import { Button } from '@/components/ui/button';
Expand All @@ -27,7 +21,7 @@ const authSchema = z.object({
password: z.string().min(1, 'Required'),
});

export const loginAction = async ({ request }: LoaderFunctionArgs) => {
export const clientAction = async ({ request }: LoaderFunctionArgs) => {
let formPayload = await request.formData();

let data = Object.fromEntries(formPayload);
Expand All @@ -52,7 +46,7 @@ export const loginAction = async ({ request }: LoaderFunctionArgs) => {
return null;
};

export const loginLoader = async () => {
export const clientLoader = async () => {
let user = useUserStore.getState().user;

if (user) {
Expand All @@ -62,15 +56,15 @@ export const loginLoader = async () => {
return null;
};

export const Auth = () => {
export default function Auth() {
let location = useLocation();
let params = new URLSearchParams(location.search);
let from = params.get('from') || '/';

let navigation = useNavigation();
let isLoggingIn = navigation.formData?.get('email') != null;

let actionData = useActionData() as Awaited<ReturnType<typeof loginAction>>;
let actionData = useActionData<typeof clientAction>();

return (
<div className='flex min-h-screen w-full flex-col items-center justify-center'>
Expand Down Expand Up @@ -106,4 +100,4 @@ export const Auth = () => {
</Form>
</div>
);
};
}
Loading

0 comments on commit 7666f5b

Please sign in to comment.