diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0405c31 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +WCL_CLIENT_ID= +WCL_CLIENT_SECRET= +REDIRECT_URL= \ No newline at end of file diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..0405c31 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,3 @@ +WCL_CLIENT_ID= +WCL_CLIENT_SECRET= +REDIRECT_URL= \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..540a8b1 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + + // Base config + extends: ["eslint:recommended"], + + 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", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + }, + }, + + // 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, + }, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-explicit-any": "warn", + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.js"], + env: { + node: true, + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfc2768 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + + +node_modules +dist +dist-ssr +/.cache +/build +/public/build +*.local + + +# env +.env +.env.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da8d02a --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Welcome to Remix! + +- [Remix Docs](https://remix.run/docs) + +## Development + +From your terminal: + +```sh +npm run dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Deployment + +First, build your app for production: + +```sh +npm run build +``` + +Then run the app in production mode: + +```sh +npm start +``` + +Now you'll need to pick a host to deploy it to. + +### DIY + +If you're familiar with deploying node applications, the built-in Remix app server is production-ready. + +Make sure to deploy the output of `remix build` + +- `build/` +- `public/build/` diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx new file mode 100644 index 0000000..174b796 --- /dev/null +++ b/app/components/Footer.tsx @@ -0,0 +1,34 @@ +function Footer() { + return ( +
+ +
+ ); +} + +export default Footer; diff --git a/app/components/WCLAuthorization.tsx b/app/components/WCLAuthorization.tsx new file mode 100644 index 0000000..43a2ecf --- /dev/null +++ b/app/components/WCLAuthorization.tsx @@ -0,0 +1,26 @@ +export const WCLAuthorization: React.FC = () => { + const handleAuthorization = async () => { + try { + const response = await fetch("/api/authorize", { + method: "POST", + }); + + if (response.ok) { + const result = await response.json(); + console.log("Success:", result.message); + } else { + const errorResult = await response.json(); + console.error("Authorization failed:", errorResult.error); + } + } catch (error) { + console.error("Error during authorization:", error); + } + }; + + return ( + <> +

Warcraft Logs Authorization

+ + + ); +}; diff --git a/app/components/WCLUrlInput.tsx b/app/components/WCLUrlInput.tsx new file mode 100644 index 0000000..ee60a1f --- /dev/null +++ b/app/components/WCLUrlInput.tsx @@ -0,0 +1,64 @@ +import { ChangeEvent, FormEvent, useState } from "react"; +import "../styles/WCLUrlInput.css"; +import { ReportParseError, parseWCLUrl } from "../wcl/gql/util/parseWCLUrl"; +import useWCLUrlInputStore from "../zustand/WCLUrlInputStore"; +import ErrorBear from "./generic/ErrorBear"; +import useStatusStore from "../zustand/statusStore"; +import { WCLAuthorization } from "./WCLAuthorization"; + +export const WCLUrlInput = () => { + const [url, setUrl] = useState(""); + const [errorBear, setErrorBear] = useState(); + + const WCLReport = useWCLUrlInputStore(); + const status = useStatusStore(); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const { reportCode, error } = parseWCLUrl(url); + if (!reportCode || error) { + if (error) { + setErrorBear(error); + } + return; + } + status.setIsFetching(true); + + setErrorBear(undefined); + status.setIsFetching(false); + }; + + const handleChange = (event: ChangeEvent) => { + setUrl(event.target.value); + }; + + return ( + <> +
+
+
+ +
+ +
+
+ {errorBear && } + {!status.hasAuth && } + {/* {!hasAuth && }*/} + + ); +}; diff --git a/app/components/generic/ErrorBear.tsx b/app/components/generic/ErrorBear.tsx new file mode 100644 index 0000000..2a0b217 --- /dev/null +++ b/app/components/generic/ErrorBear.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import "../../styles/genericStyling.css"; +import { + ReportParseError, + reportParseErrorIconMap, + reportParseErrorMap, +} from "../../wcl/gql/util/parseWCLUrl"; + +interface ErrorBearProps { + error: ReportParseError; + customMsg?: string; +} + +const ErrorBear: React.FC = ({ error, customMsg }) => ( +
+ An error occurred: +

{customMsg ? customMsg : reportParseErrorMap[error]}

+
+); + +export default ErrorBear; diff --git a/app/entry.client.tsx b/app/entry.client.tsx new file mode 100644 index 0000000..94d5dc0 --- /dev/null +++ b/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * 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"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/app/entry.server.tsx b/app/entry.server.tsx new file mode 100644 index 0000000..e2002b0 --- /dev/null +++ b/app/entry.server.tsx @@ -0,0 +1,140 @@ +/** + * By default, Remix will handle generating the HTTP Response 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.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import isbot from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent")) + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/app/root.tsx b/app/root.tsx new file mode 100644 index 0000000..cd8a8c6 --- /dev/null +++ b/app/root.tsx @@ -0,0 +1,39 @@ +import { cssBundleHref } from "@remix-run/css-bundle"; +import type { LinksFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +import "./styles/fonts.css"; +import "./styles/App.css"; + +export const links: LinksFunction = () => [ + ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), +]; + +export default function App() { + return ( + + + + + + + + + +
+ + + + +
+ + + ); +} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx new file mode 100644 index 0000000..7ee80bb --- /dev/null +++ b/app/routes/_index.tsx @@ -0,0 +1,18 @@ +import type { MetaFunction } from "@remix-run/node"; +import "../styles/index.css"; +import { WCLUrlInput } from "../components/WCLUrlInput"; +import Footer from "../components/Footer"; + +export const meta: MetaFunction = () => { + return [{ title: "Analysis Tools" }]; +}; + +export default function Index() { + return ( + <> +

WCL URL

+ +