Skip to content

Commit

Permalink
chore: move remix folder content to root
Browse files Browse the repository at this point in the history
  • Loading branch information
meienberger committed Sep 3, 2024
1 parent 1b690e1 commit 19eff61
Show file tree
Hide file tree
Showing 51 changed files with 648 additions and 1,643 deletions.
File renamed without changes.
41 changes: 4 additions & 37 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,37 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
/data
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
build/
node_modules/
games/
.cache/
2 changes: 1 addition & 1 deletion remix/Dockerfile → Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base

FROM node_base AS builder_base

RUN npm install pnpm -g
RUN npm install pnpm@9 -g

# BUILDER
FROM builder_base AS builder
Expand Down
65 changes: 65 additions & 0 deletions app/components/delete-game-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Button, buttonVariants } from "./ui/button";
import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
import type { GameMetadata } from "@/server/data";
import { z } from "zod";
import toast from "react-hot-toast";

export const DeleteGameDialog = (props: { game: GameMetadata }) => {
const { game } = props;
const [open, setOpen] = useState(false);

const fetcher = useFetcher();
const isSubmitting = fetcher.state !== "idle";

const res = z
.object({ success: z.boolean() })
.or(z.undefined())
.parse(fetcher.data);

useEffect(() => {
if (res?.success) {
setOpen(false);
toast.success("Game deleted successfully");
}
}, [res?.success]);

return (
<Dialog open={open} onOpenChange={(o) => setOpen(o)}>
<DialogTrigger>
<div className={buttonVariants({ variant: "destructive" })}>Delete</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Delete game ${game.name}?`}</DialogTitle>
<DialogDescription>
Are you sure you want to delete this game? All of your data will be
permanently removed. This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<fetcher.Form action="delete-game" method="post">
<input type="hidden" name="gameId" value={game.gameId} />
<Button
variant="destructive"
type="submit"
disabled={isSubmitting}
aria-disabled={isSubmitting}
>
Delete
</Button>
</fetcher.Form>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
70 changes: 70 additions & 0 deletions app/components/delete-save-state-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Button, buttonVariants } from "./ui/button";
import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
import { z } from "zod";
import toast from "react-hot-toast";

type Props = {
saveId: string;
};

export const DeleteSaveStateDialog = (props: Props) => {
const { saveId } = props;
const [open, setOpen] = useState(false);

const fetcher = useFetcher();
const isSubmitting = fetcher.state !== "idle";

const res = z
.object({ success: z.boolean() })
.or(z.undefined())
.parse(fetcher.data);

useEffect(() => {
if (res?.success) {
toast.success("Save state deleted successfully");
setOpen(false);
}
}, [res?.success]);

return (
<Dialog open={open} onOpenChange={(o) => setOpen(o)}>
<DialogTrigger>
<div className={buttonVariants({ variant: "destructive", size: "sm" })}>
Delete
</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete save state?</DialogTitle>
<DialogDescription>
Are you sure you want to delete this save? You won't be able to
recover it.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<fetcher.Form action="delete-save" method="post">
<input type="hidden" name="saveId" value={saveId} />
<Button
variant="destructive"
type="submit"
disabled={isSubmitting}
aria-disabled={isSubmitting}
>
Delete
</Button>
</fetcher.Form>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { useRevalidate } from "@/hooks/use-revalidate";
import Iframe from "./iframe";
import { useFetcher } from "@remix-run/react";

export const GamePlayer = (props: { metadata: GameMetadata; saveStateToLoad: string }) => {
export const GamePlayer = (props: {
metadata: GameMetadata;
saveStateToLoad: string;
}) => {
const { metadata, saveStateToLoad } = props;

const revalidate = useRevalidate();
Expand Down Expand Up @@ -52,28 +55,9 @@ export const GamePlayer = (props: { metadata: GameMetadata; saveStateToLoad: str
{
action: `/play/${metadata.gameId}/save`,
method: "POST",
encType: "application/json",
},
);

// const res = await fetch(`/play/${metadata.gameId}/save`, {
// method: "POST",
// body: JSON.stringify({
// state: base64save,
// screenshot: base64screenshot,
// auto,
// gameId: metadata.gameId,
// }),
// headers: {
// "Content-Type": "application/json",
// },
// });

// if (!res.ok) {
// toast.error("Failed to save game");
// } else {
// toast.success("Game saved");
// revalidate();
// }
} catch (e) {
console.error(e);
}
Expand Down
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.
File renamed without changes.
File renamed without changes.
File renamed without changes.
85 changes: 85 additions & 0 deletions app/components/upload-game-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Button, buttonVariants } from "@/components/ui/button";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogHeader,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { z } from "zod";

export const UploadGameDialog = () => {
const [open, setOpen] = useState(false);

const fetcher = useFetcher();
const isSubmitting = fetcher.state !== "idle";

const res = z
.object({ errors: z.record(z.string()), success: z.boolean() })
.or(z.undefined())
.parse(fetcher.data);

useEffect(() => {
if (res?.success) {
setOpen(false);
toast.success("Game uploaded successfully");
}
}, [res?.success]);

return (
<Dialog open={open} onOpenChange={(o) => setOpen(o)}>
<DialogTrigger>
<div className={buttonVariants()}>Upload a game</div>
</DialogTrigger>
<DialogContent>
<fetcher.Form
id="upload-game-form"
method="post"
action="/upload-game"
encType="multipart/form-data"
>
<DialogHeader>
<DialogTitle>Upload a game</DialogTitle>
<DialogDescription>
GBA, GBC, and GB file types are supported.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 my-4">
<Input
type="file"
name="game_file"
required
accept=".gba,.gbc,.gb"
/>
{res?.errors.game_file && (
<p id="game_file" className="ml-3 text-sm text-red-500 mb-2">
{res.errors.game_file}
</p>
)}
<Input type="text" name="name" placeholder="Name" required />
{res?.errors.name && (
<p id="name" className="ml-3 text-sm text-red-500">
{res.errors.name}
</p>
)}
</div>
<DialogFooter>
<Button
type="submit"
disabled={isSubmitting}
aria-disabled={isSubmitting}
>
Upload
</Button>
</DialogFooter>
</fetcher.Form>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback } from "react";
import { useNavigate } from "@remix-run/react";

export const useRevalidate = () => {
let navigate = useNavigate();
const navigate = useNavigate();
return useCallback(
function revalidate() {
navigate({ pathname: ".", search: window.location.search });
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
43 changes: 43 additions & 0 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { GamesTable } from "@/components/games-table";
import { UploadGameDialog } from "@/components/upload-game-dialog";
import { getGamesMetadata } from "@/server/data";
import { json, useLoaderData } from "@remix-run/react";

export const loader = async () => {
const games = await getGamesMetadata();

return json({ games });
};

export default function Index() {
const { games } = useLoaderData<typeof loader>();

return (
<section>
<div className="relative items-center w-full px-5 py-12 mx-auto md:px-12 lg:px-16 max-w-7xl lg:py-24">
<div className="flex w-full mx-auto text-left">
<div className="relative inline-flex items-center mx-auto align-middle">
<div className="text-center">
<h1 className="max-w-5xl text-2xl font-bold leading-none tracking-tighter md:text-5xl lg:text-6xl lg:max-w-7xl bg-gradient-to-r from-red-500 to-blue-500 bg-clip-text text-transparent">
NextGBA
</h1>
<p className="max-w-xl mx-auto mt-4 text-base leading-relaxed text-gray-500">
Play your favorite GBA games on the web.
<br />
Powered by <b>EmulatorJS</b>.
</p>
<div className="flex justify-center w-full max-w-2xl gap-2 mx-auto mt-6">
<div className="mt-3 rounded-lg sm:mt-0">
<UploadGameDialog />
</div>
</div>
</div>
</div>
</div>
<div className="w-full mx-auto border-2 p-2 border-dashed mt-24 rounded">
<GamesTable games={games} />
</div>
</div>
</section>
);
}
26 changes: 26 additions & 0 deletions app/routes/api.rom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { getRomFile } from "@/server/data";

export async function loader({ request }: LoaderFunctionArgs) {
try {
const { searchParams } = new URL(request.url);
const gameId = searchParams.get("gameId");
const csl = searchParams.get("console");

if (typeof gameId !== "string" || typeof csl !== "string") {
return new Response("Not found", { status: 404 });
}

const file = await getRomFile(gameId, csl);

return new Response(file, {
headers: {
"content-type": "application/octet-stream",
"cache-control": "public, max-age=31536000",
},
});
} catch (error) {
console.error(error);
return new Response("Error", { status: 500 });
}
}
Loading

0 comments on commit 19eff61

Please sign in to comment.