mirror of
https://github.com/tubearchivist/tubearchivist-frontend.git
synced 2024-11-21 19:30:16 +00:00
Updates
Signed-off-by: Sean Norwood <norwood.sean@gmail.com>
This commit is contained in:
parent
7389ed0b2b
commit
470c7a136f
9
.gitpod.Dockerfile
vendored
9
.gitpod.Dockerfile
vendored
@ -1,9 +0,0 @@
|
|||||||
FROM gitpod/workspace-full
|
|
||||||
|
|
||||||
# Install Fly
|
|
||||||
RUN curl -L https://fly.io/install.sh | sh
|
|
||||||
ENV FLYCTL_INSTALL="/home/gitpod/.fly"
|
|
||||||
ENV PATH="$FLYCTL_INSTALL/bin:$PATH"
|
|
||||||
|
|
||||||
# Install GitHub CLI
|
|
||||||
RUN brew install gh
|
|
48
.gitpod.yml
48
.gitpod.yml
@ -1,48 +0,0 @@
|
|||||||
# https://www.gitpod.io/docs/config-gitpod-file
|
|
||||||
|
|
||||||
image:
|
|
||||||
file: .gitpod.Dockerfile
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- port: 3000
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Restore .env file
|
|
||||||
command: |
|
|
||||||
if [ -f .env ]; then
|
|
||||||
# If this workspace already has a .env, don't override it
|
|
||||||
# Local changes survive a workspace being opened and closed
|
|
||||||
# but they will not persist between separate workspaces for the same repo
|
|
||||||
|
|
||||||
echo "Found .env in workspace"
|
|
||||||
else
|
|
||||||
# There is no .env
|
|
||||||
if [ ! -n "${ENV}" ]; then
|
|
||||||
# There is no $ENV from a previous workspace
|
|
||||||
# Default to the example .env
|
|
||||||
echo "Setting example .env"
|
|
||||||
|
|
||||||
cp .env.example .env
|
|
||||||
else
|
|
||||||
# After making changes to .env, run this line to persist it to $ENV
|
|
||||||
# eval $(gp env -e ENV="$(base64 .env | tr -d '\n')")
|
|
||||||
#
|
|
||||||
# Environment variables set this way are shared between all your workspaces for this repo
|
|
||||||
# The lines below will read $ENV and print a .env file
|
|
||||||
|
|
||||||
echo "Restoring .env from Gitpod"
|
|
||||||
|
|
||||||
echo "${ENV}" | base64 -d | tee .env > /dev/null
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- init: npm install
|
|
||||||
command: npm run setup && npm run dev
|
|
||||||
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- ms-azuretools.vscode-docker
|
|
||||||
- esbenp.prettier-vscode
|
|
||||||
- dbaeumer.vscode-eslint
|
|
||||||
- bradlc.vscode-tailwindcss
|
|
@ -5,10 +5,10 @@ type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Layout: React.FC<Props> = ({ children }) => {
|
export const Layout = ({ children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ minHeight: "100vh" }} className="main-content">
|
<div className="main-content">
|
||||||
<Nav />
|
<Nav />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import IconPlay from "~/images/icon-play.svg";
|
import IconPlay from "~/images/icon-play.svg";
|
||||||
import type { Videos } from "~/routes/index";
|
|
||||||
import type { Datum } from "~/types/Videos";
|
|
||||||
import VideoPlayer from "../VideoPlayer/VideoPlayer";
|
import VideoPlayer from "../VideoPlayer/VideoPlayer";
|
||||||
|
import type { Datum } from "~/types/Videos";
|
||||||
|
|
||||||
type ViewStyle = "grid grid-3" | "list";
|
type ViewStyle = "grid grid-3" | "list";
|
||||||
|
|
||||||
const VideoList = ({ videos }: { videos: Videos }) => {
|
const VideoList = ({ videos }: { videos: Datum[] }) => {
|
||||||
const [selectedVideoUrl, setSelectedVideoUrl] = useState<Datum>();
|
const [selectedVideoUrl, setSelectedVideoUrl] = useState<Datum>();
|
||||||
const [viewStyle, setViewStyle] = useState<ViewStyle>("grid grid-3");
|
const [viewStyle, setViewStyle] = useState<ViewStyle>("grid grid-3");
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ const VideoList = ({ videos }: { videos: Videos }) => {
|
|||||||
setViewStyle(selectedViewStyle);
|
setViewStyle(selectedViewStyle);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (videos.length < 1) {
|
if (!videos) {
|
||||||
return (
|
return (
|
||||||
<div className="boxed-content">
|
<div className="boxed-content">
|
||||||
<h2>No videos found...</h2>
|
<h2>No videos found...</h2>
|
||||||
@ -110,7 +109,7 @@ const VideoList = ({ videos }: { videos: Videos }) => {
|
|||||||
<div style={{ cursor: "pointer" }} onClick={() => handleSelectedVideo(video)}>
|
<div style={{ cursor: "pointer" }} onClick={() => handleSelectedVideo(video)}>
|
||||||
<div className={`video-thumb-wrap ${viewStyle}`}>
|
<div className={`video-thumb-wrap ${viewStyle}`}>
|
||||||
<div className="video-thumb">
|
<div className="video-thumb">
|
||||||
<img src={video.resolved_thumb_url} alt="video-thumb" />
|
<img src={`http://localhost:8000${video.vid_thumb_url}`} alt="video-thumb" />
|
||||||
{/* {% if video.source.player.progress %} */}
|
{/* {% if video.source.player.progress %} */}
|
||||||
<div
|
<div
|
||||||
className="video-progress-bar"
|
className="video-progress-bar"
|
||||||
|
5
app/cookies.ts
Normal file
5
app/cookies.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createCookie } from "@remix-run/node";
|
||||||
|
|
||||||
|
export const csrfCookie = createCookie("csrftoken", {
|
||||||
|
maxAge: 604_800,
|
||||||
|
});
|
@ -1,3 +1,2 @@
|
|||||||
export const API_KEY =
|
export const API_KEY = process.env.API_KEY;
|
||||||
process.env.API_KEY || `1954c006f731df60bf4c9f027d6b7076d699d319`;
|
export const API_URL = process.env.API_URL;
|
||||||
export const API_URL = process.env.API_URL || `https://tube.stiforr.tech`;
|
|
||||||
|
@ -18,13 +18,10 @@ export const getChannels = async (token: string): Promise<Channels> => {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Error getting channel information");
|
throw new Error("Error getting channel information");
|
||||||
}
|
}
|
||||||
return response.json();
|
return await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getChannel = async (
|
export const getChannel = async (token: string, id: string | undefined): Promise<Channel> => {
|
||||||
token: string,
|
|
||||||
id: string | undefined
|
|
||||||
): Promise<Channel> => {
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error(`Unable to fetch channels, no token provided`);
|
throw new Error(`Unable to fetch channels, no token provided`);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import type { Datum, Videos } from "~/types/Videos";
|
import type { Datum, Videos } from "~/types/Videos";
|
||||||
import { API_URL } from "./constants.server";
|
import { API_URL } from "./constants.server";
|
||||||
|
import { getSession } from "~/session.server";
|
||||||
|
|
||||||
export const getVideos = async (token: string): Promise<Videos> => {
|
export const getVideos = async (request: Request) => {
|
||||||
if (!token) {
|
// if (!token) {
|
||||||
throw new Error("Missing API token in request to get videos");
|
// throw new Error("Missing API token in request to get videos");
|
||||||
}
|
// }
|
||||||
|
const session = await getSession(request);
|
||||||
|
const token = session.get("token");
|
||||||
const response = await fetch(`${API_URL}/api/video/`, {
|
const response = await fetch(`${API_URL}/api/video/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
@ -15,10 +18,15 @@ export const getVideos = async (token: string): Promise<Videos> => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// if (response.status === 403) {
|
||||||
|
// return redirect("/login");
|
||||||
|
// }
|
||||||
throw new Error(`Failed to fetch videos: ${response.statusText}`);
|
throw new Error(`Failed to fetch videos: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
const data: Videos = await response.json();
|
||||||
|
|
||||||
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getVideo = async (token: string, videoId: string): Promise<Datum> => {
|
export const getVideo = async (token: string, videoId: string): Promise<Datum> => {
|
||||||
|
@ -10,11 +10,10 @@ import {
|
|||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
import { Layout } from "./components/Layout";
|
import { Layout } from "./components/Layout";
|
||||||
import styles from "./styles/dark.css";
|
import styles from "./styles/dark.css";
|
||||||
import global from "./styles/globals.css";
|
import global from "./styles/style.css";
|
||||||
|
|
||||||
export const links: LinksFunction = () => {
|
export const links: LinksFunction = () => {
|
||||||
return [
|
return [
|
||||||
// { rel: "stylesheet", href: tailwindStylesheetUrl },
|
|
||||||
{ rel: "stylesheet", href: styles },
|
{ rel: "stylesheet", href: styles },
|
||||||
{ rel: "stylesheet", href: global },
|
{ rel: "stylesheet", href: global },
|
||||||
];
|
];
|
||||||
@ -35,14 +34,14 @@ export async function loader({ request }: LoaderArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const data = useLoaderData();
|
const data = useLoaderData<typeof loader>();
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="h-full">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body className="h-full">
|
<body>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
33
app/routes/_index.tsx
Normal file
33
app/routes/_index.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useLoaderData } from "@remix-run/react";
|
||||||
|
import type { ErrorBoundaryComponent, LoaderArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import VideoList from "~/components/VideoList/VideoList";
|
||||||
|
import { getVideos } from "~/lib/getVideos";
|
||||||
|
|
||||||
|
export const loader = async ({ context, request }: LoaderArgs) => {
|
||||||
|
const data = await getVideos(request);
|
||||||
|
return json(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const { data: videos } = useLoaderData<typeof loader>();
|
||||||
|
// console.log(videos);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<VideoList videos={videos} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }: { error: Error }) => {
|
||||||
|
console.warn(error);
|
||||||
|
return (
|
||||||
|
<div className="boxed-content">
|
||||||
|
<div className="title-bar">
|
||||||
|
<h1>Error: </h1>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -5,7 +5,7 @@ import { API_URL } from "~/lib/constants";
|
|||||||
import { API_KEY } from "~/lib/constants.server";
|
import { API_KEY } from "~/lib/constants.server";
|
||||||
import { getChannels } from "~/lib/getChannels";
|
import { getChannels } from "~/lib/getChannels";
|
||||||
import type { Channels } from "~/types/channels";
|
import type { Channels } from "~/types/channels";
|
||||||
import { formatNumbers } from "../../lib/utils";
|
import { formatNumbers } from "../lib/utils";
|
||||||
|
|
||||||
export const loader: LoaderFunction = async () => {
|
export const loader: LoaderFunction = async () => {
|
||||||
const channels = await getChannels(API_KEY);
|
const channels = await getChannels(API_KEY);
|
||||||
@ -93,10 +93,7 @@ const ChannelsPage = () => {
|
|||||||
) : (
|
) : (
|
||||||
channels.data?.map((channel) => {
|
channels.data?.map((channel) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={channel.channel_id} className={`channel-item ${viewStyle}`}>
|
||||||
key={channel.channel_id}
|
|
||||||
className={`channel-item ${viewStyle}`}
|
|
||||||
>
|
|
||||||
<div className={`channel-banner ${viewStyle}`}>
|
<div className={`channel-banner ${viewStyle}`}>
|
||||||
<Link to={channel.channel_id}>
|
<Link to={channel.channel_id}>
|
||||||
<img
|
<img
|
||||||
@ -109,22 +106,15 @@ const ChannelsPage = () => {
|
|||||||
<div className="info-box-item">
|
<div className="info-box-item">
|
||||||
<div className="round-img">
|
<div className="round-img">
|
||||||
<Link to={channel.channel_id}>
|
<Link to={channel.channel_id}>
|
||||||
<img
|
<img src={`${API_URL}${channel.channel_thumb_url}`} alt="channel-thumb" />
|
||||||
src={`${API_URL}${channel.channel_thumb_url}`}
|
|
||||||
alt="channel-thumb"
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>
|
<h3>
|
||||||
<Link to={channel.channel_id}>
|
<Link to={channel.channel_id}>{channel?.channel_name}</Link>
|
||||||
{channel?.channel_name}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
</h3>
|
||||||
{/* {% if channel.source.channel_subs >= 1000000 %} */}
|
{/* {% if channel.source.channel_subs >= 1000000 %} */}
|
||||||
<p>
|
<p>Subscribers: {formatNumbers(channel?.channel_subs)}</p>
|
||||||
Subscribers: {formatNumbers(channel?.channel_subs)}
|
|
||||||
</p>
|
|
||||||
{/* {% else %} */}
|
{/* {% else %} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,20 +126,12 @@ const ChannelsPage = () => {
|
|||||||
className="unsubscribe"
|
className="unsubscribe"
|
||||||
type="button"
|
type="button"
|
||||||
id="{{ channel.source.channel_id }}"
|
id="{{ channel.source.channel_id }}"
|
||||||
onClick={() =>
|
onClick={() => console.log("unsubscribe(this.id) -> toggleSubscribe()")}
|
||||||
console.log(
|
|
||||||
"unsubscribe(this.id) -> toggleSubscribe()"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={`${
|
title={`${
|
||||||
channel?.channel_subscribed
|
channel?.channel_subscribed ? "Unsubscribe from" : "Subscribe to"
|
||||||
? "Unsubscribe from"
|
|
||||||
: "Subscribe to"
|
|
||||||
} ${channel?.channel_name}`}
|
} ${channel?.channel_name}`}
|
||||||
>
|
>
|
||||||
{channel?.channel_subscribed
|
{channel?.channel_subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
? "Unsubscribe"
|
|
||||||
: "Subscribe"}
|
|
||||||
</button>
|
</button>
|
||||||
{/* {% else %} */}
|
{/* {% else %} */}
|
||||||
{/* <button
|
{/* <button
|
@ -1,43 +0,0 @@
|
|||||||
import { useLoaderData } from "@remix-run/react";
|
|
||||||
import type { ErrorBoundaryComponent, LoaderFunction } from "@remix-run/server-runtime";
|
|
||||||
|
|
||||||
import VideoList from "~/components/VideoList/VideoList";
|
|
||||||
import { API_KEY } from "~/lib/constants.server";
|
|
||||||
import { getVideos } from "~/lib/getVideos";
|
|
||||||
import type { Datum } from "~/types/Videos";
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ context, request }) => {
|
|
||||||
const { data } = await getVideos(API_KEY);
|
|
||||||
const withVideoThumbs = data.map((d) => ({
|
|
||||||
...d,
|
|
||||||
resolved_thumb_url: `${process.env.PUBLIC_API_URL}${d.vid_thumb_url}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return withVideoThumbs;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Videos = Datum[] & {
|
|
||||||
resolved_thumb_url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const videos = useLoaderData<Videos>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<VideoList videos={videos} />
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }: { error: Error }) => {
|
|
||||||
console.warn(error);
|
|
||||||
return (
|
|
||||||
<div className="boxed-content">
|
|
||||||
<div className="title-bar">
|
|
||||||
<h1>Error: </h1>
|
|
||||||
<p>{error.message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
106
app/routes/login.tsx
Normal file
106
app/routes/login.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Form, useLoaderData, useSearchParams } from "@remix-run/react";
|
||||||
|
import type { ActionArgs, ErrorBoundaryComponent, LoaderArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import { csrfCookie } from "~/cookies";
|
||||||
|
import { createCsrfToken, createUserSession, login } from "~/session.server";
|
||||||
|
import { safeRedirect } from "~/utils";
|
||||||
|
|
||||||
|
export const loader = async ({ request }: LoaderArgs) => {
|
||||||
|
const csrf = await createCsrfToken(request);
|
||||||
|
return json(csrf, {
|
||||||
|
headers: {
|
||||||
|
"Set-Cookie": await csrfCookie.serialize(csrf),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const action = async ({ request }: ActionArgs) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const username = formData.get("username");
|
||||||
|
const password = formData.get("password");
|
||||||
|
const redirectTo = safeRedirect(formData.get("next"), "/");
|
||||||
|
const remember = formData.get("remember");
|
||||||
|
|
||||||
|
const user = await login({ username, password, request });
|
||||||
|
console.log(user);
|
||||||
|
|
||||||
|
return createUserSession({
|
||||||
|
redirectTo,
|
||||||
|
request,
|
||||||
|
userId: user.user_id,
|
||||||
|
token: user.token,
|
||||||
|
remember: remember === "on" ? true : false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const redirectTo = searchParams.get("redirectTo") || "/";
|
||||||
|
const csrf = useLoaderData<typeof loader>();
|
||||||
|
console.log(csrf);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="boxed-content login-page">
|
||||||
|
<img src="img/logo-tube-archivist-dark.png" alt="tube-archivist-logo" />
|
||||||
|
|
||||||
|
<h1>Tube Archivist</h1>
|
||||||
|
<h2>Your Self Hosted YouTube Media Server</h2>
|
||||||
|
|
||||||
|
<p className="danger-zone">Failed to login.</p>
|
||||||
|
|
||||||
|
<Form method="POST" name="login">
|
||||||
|
<input type="text" name="username" />
|
||||||
|
<br />
|
||||||
|
<input type="password" name="password" />
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Remember me: <input type="checkbox" name="remember" />
|
||||||
|
</p>
|
||||||
|
<input type="hidden" name="next" value={redirectTo} />
|
||||||
|
<input type="hidden" name="csrf" value={csrf} />
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</Form>
|
||||||
|
<p className="login-links">
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/tubearchivist/tubearchivist"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</a>
|
||||||
|
</span>{" "}
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/tubearchivist/tubearchivist#donate"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Donate
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="footer-colors">
|
||||||
|
<div className="col-1"></div>
|
||||||
|
<div className="col-2"></div>
|
||||||
|
<div className="col-3"></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
|
|
||||||
|
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }: { error: Error }) => {
|
||||||
|
console.warn(error);
|
||||||
|
return (
|
||||||
|
<div className="boxed-content">
|
||||||
|
<div className="title-bar">
|
||||||
|
<h1>Error: </h1>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
6
app/routes/logout.tsx
Normal file
6
app/routes/logout.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||||
|
import { logout } from "~/session.server";
|
||||||
|
|
||||||
|
export const loader = async ({ request }: LoaderArgs) => {
|
||||||
|
return logout(request);
|
||||||
|
};
|
@ -1,15 +1,14 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Link, useLoaderData } from "@remix-run/react";
|
import { Link, useLoaderData } from "@remix-run/react";
|
||||||
|
import { useState } from "react";
|
||||||
import IconAdd from "~/images/icon-add.svg";
|
import IconAdd from "~/images/icon-add.svg";
|
||||||
import IconGridView from "~/images/icon-gridview.svg";
|
import IconGridView from "~/images/icon-gridview.svg";
|
||||||
import IconListView from "~/images/icon-listview.svg";
|
import IconListView from "~/images/icon-listview.svg";
|
||||||
import type { LoaderFunction } from "@remix-run/server-runtime";
|
|
||||||
import { getPlaylists } from "~/lib/getPlaylists";
|
|
||||||
import { API_KEY } from "~/lib/constants.server";
|
|
||||||
import type { Playlists } from "~/types/playlists";
|
|
||||||
import { API_URL } from "~/lib/constants";
|
import { API_URL } from "~/lib/constants";
|
||||||
|
import { API_KEY } from "~/lib/constants.server";
|
||||||
|
import { getPlaylists } from "~/lib/getPlaylists";
|
||||||
|
import type { Playlists } from "~/types/playlists";
|
||||||
|
|
||||||
export const loader: LoaderFunction = async () => {
|
export const loader = async () => {
|
||||||
const playlists = await getPlaylists(API_KEY);
|
const playlists = await getPlaylists(API_KEY);
|
||||||
|
|
||||||
return playlists;
|
return playlists;
|
||||||
@ -97,10 +96,7 @@ const Playlist = () => {
|
|||||||
) : (
|
) : (
|
||||||
playlists.data.map((playlist) => {
|
playlists.data.map((playlist) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={playlist.playlist_id} className={`playlist-item ${viewStyle}`}>
|
||||||
key={playlist.playlist_id}
|
|
||||||
className={`playlist-item ${viewStyle}`}
|
|
||||||
>
|
|
||||||
<div className="playlist-thumbnail">
|
<div className="playlist-thumbnail">
|
||||||
<Link to={playlist.playlist_id}>
|
<Link to={playlist.playlist_id}>
|
||||||
<img
|
<img
|
@ -1,9 +1,8 @@
|
|||||||
import { createCookieSessionStorage, redirect } from "@remix-run/node";
|
import { createCookieSessionStorage, json, redirect } from "@remix-run/node";
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
|
import { API_URL } from "./lib/constants.server";
|
||||||
import type { User } from "~/models/user.server";
|
import { csrfCookie } from "./cookies";
|
||||||
import { getUserById } from "~/models/user.server";
|
|
||||||
|
|
||||||
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
|
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
|
||||||
|
|
||||||
export const sessionStorage = createCookieSessionStorage({
|
export const sessionStorage = createCookieSessionStorage({
|
||||||
@ -17,6 +16,17 @@ export const sessionStorage = createCookieSessionStorage({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const csrfStorage = createCookieSessionStorage({
|
||||||
|
cookie: {
|
||||||
|
name: "csrftoken",
|
||||||
|
httpOnly: false,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secrets: [process.env.SESSION_SECRET],
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const USER_SESSION_KEY = "userId";
|
const USER_SESSION_KEY = "userId";
|
||||||
|
|
||||||
export async function getSession(request: Request) {
|
export async function getSession(request: Request) {
|
||||||
@ -24,61 +34,121 @@ export async function getSession(request: Request) {
|
|||||||
return sessionStorage.getSession(cookie);
|
return sessionStorage.getSession(cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserId(
|
export const getCsrfSession = async (request: Request) => {
|
||||||
request: Request
|
const cookie = request.headers.get("Cookie");
|
||||||
): Promise<User["id"] | undefined> {
|
return csrfStorage.getSession(cookie);
|
||||||
const session = await getSession(request);
|
};
|
||||||
const userId = session.get(USER_SESSION_KEY);
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUser(request: Request) {
|
export const createCsrfToken = async (request: Request) => {
|
||||||
const userId = await getUserId(request);
|
const cookieHeaders = request.headers.get("Cookie");
|
||||||
if (userId === undefined) return null;
|
const cookie = await csrfCookie.parse(cookieHeaders);
|
||||||
|
|
||||||
const user = await getUserById(userId);
|
if (cookie) {
|
||||||
if (user) return user;
|
return cookie;
|
||||||
|
|
||||||
throw await logout(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requireUserId(
|
|
||||||
request: Request,
|
|
||||||
redirectTo: string = new URL(request.url).pathname
|
|
||||||
) {
|
|
||||||
const userId = await getUserId(request);
|
|
||||||
if (!userId) {
|
|
||||||
const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
|
|
||||||
throw redirect(`/login?${searchParams}`);
|
|
||||||
}
|
}
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requireUser(request: Request) {
|
const token = randomBytes(100).toString("base64");
|
||||||
const userId = await requireUserId(request);
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
const user = await getUserById(userId);
|
type LoginResponse = {
|
||||||
if (user) return user;
|
token: string;
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
throw await logout(request);
|
export const login = async ({
|
||||||
}
|
username,
|
||||||
|
password,
|
||||||
|
request,
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
request: Request;
|
||||||
|
}) => {
|
||||||
|
if (!username || !password) {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
const csrfmiddlewaretoken = csrfCookie.parse(request.headers.get("Cookie"));
|
||||||
|
const creds = JSON.stringify({ username, password, csrfmiddlewaretoken });
|
||||||
|
const response = await fetch(`${API_URL}/api/login/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: creds,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept-Language": "en-US",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to login: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const cookie = response.headers.get("Cookie");
|
||||||
|
const data: LoginResponse = await response.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export async function getUserId(
|
||||||
|
// request: Request
|
||||||
|
// ): Promise<User["id"] | undefined> {
|
||||||
|
// const session = await getSession(request);
|
||||||
|
// const userId = session.get(USER_SESSION_KEY);
|
||||||
|
// return userId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function getUser(request: Request) {
|
||||||
|
// const userId = await getUserId(request);
|
||||||
|
// if (userId === undefined) return null;
|
||||||
|
|
||||||
|
// const user = await getUserById(userId);
|
||||||
|
// if (user) return user;
|
||||||
|
|
||||||
|
// throw await logout(request);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function requireUserId(
|
||||||
|
// request: Request,
|
||||||
|
// redirectTo: string = new URL(request.url).pathname
|
||||||
|
// ) {
|
||||||
|
// const userId = await getUserId(request);
|
||||||
|
// if (!userId) {
|
||||||
|
// const searchParams = new URLSearchParams([["redirectTo", redirectTo]]);
|
||||||
|
// throw redirect(`/login?${searchParams}`);
|
||||||
|
// }
|
||||||
|
// return userId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function requireUser(request: Request) {
|
||||||
|
// const userId = await requireUserId(request);
|
||||||
|
|
||||||
|
// const user = await getUserById(userId);
|
||||||
|
// if (user) return user;
|
||||||
|
|
||||||
|
// throw await logout(request);
|
||||||
|
// }
|
||||||
|
|
||||||
export async function createUserSession({
|
export async function createUserSession({
|
||||||
request,
|
request,
|
||||||
userId,
|
userId,
|
||||||
remember,
|
token,
|
||||||
|
remember = false,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
}: {
|
}: {
|
||||||
request: Request;
|
request: Request;
|
||||||
userId: string;
|
userId: string;
|
||||||
remember: boolean;
|
token: string;
|
||||||
|
remember?: boolean;
|
||||||
redirectTo: string;
|
redirectTo: string;
|
||||||
}) {
|
}) {
|
||||||
const session = await getSession(request);
|
// const session = await getSession(request);
|
||||||
session.set(USER_SESSION_KEY, userId);
|
const csrf = await getCsrfSession(request);
|
||||||
|
|
||||||
|
// csrf.set(USER_SESSION_KEY, userId);
|
||||||
|
// csrf.set("token", token);
|
||||||
|
csrf.set("csrftoken", token);
|
||||||
|
|
||||||
return redirect(redirectTo, {
|
return redirect(redirectTo, {
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": await sessionStorage.commitSession(session, {
|
"Set-Cookie": await csrfStorage.commitSession(csrf, {
|
||||||
maxAge: remember
|
maxAge: remember
|
||||||
? 60 * 60 * 24 * 7 // 7 days
|
? 60 * 60 * 24 * 7 // 7 days
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -88,10 +158,10 @@ export async function createUserSession({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(request: Request) {
|
export async function logout(request: Request) {
|
||||||
const session = await getSession(request);
|
const session = await getCsrfSession(request);
|
||||||
return redirect("/", {
|
return redirect("/login", {
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": await sessionStorage.destroySession(session),
|
"Set-Cookie": await csrfStorage.destroySession(session),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
0
app/styles/dark.css
Normal file → Executable file
0
app/styles/dark.css
Normal file → Executable file
0
app/styles/light.css
Normal file → Executable file
0
app/styles/light.css
Normal file → Executable file
@ -125,6 +125,10 @@ button:hover {
|
|||||||
color: var(--main-bg);
|
color: var(--main-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-box {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.unsubscribe {
|
.unsubscribe {
|
||||||
background-color: var(--accent-font-light);
|
background-color: var(--accent-font-light);
|
||||||
}
|
}
|
||||||
@ -349,6 +353,7 @@ button:hover {
|
|||||||
.grid-count {
|
.grid-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-icons img {
|
.view-icons img {
|
||||||
@ -369,6 +374,10 @@ button:hover {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#hidden-form button {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
#text-reveal {
|
#text-reveal {
|
||||||
height: 0;
|
height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -391,6 +400,11 @@ button:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
align-content: space-evenly;
|
align-content: space-evenly;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
position: relative; /* needed for modal */
|
||||||
|
}
|
||||||
|
|
||||||
|
#notifications {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notifications {
|
.notifications {
|
||||||
@ -460,7 +474,7 @@ video:-webkit-full-screen {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-item:hover .video-thumb span {
|
.video-item:hover .video-tags {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +485,8 @@ video:-webkit-full-screen {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-progress-bar {
|
.video-progress-bar,
|
||||||
|
.notification-progress-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: var(--accent-font-dark);
|
background-color: var(--accent-font-dark);
|
||||||
height: 7px;
|
height: 7px;
|
||||||
@ -484,16 +499,20 @@ video:-webkit-full-screen {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-thumb span {
|
.video-tags {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
left: 5px;
|
left: 0;
|
||||||
background-color: var(--accent-font-light);
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: 300ms ease-in-out;
|
transition: 300ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-tags span {
|
||||||
|
background-color: var(--accent-font-light);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.video-play img {
|
.video-play img {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
filter: var(--img-filter);
|
filter: var(--img-filter);
|
||||||
@ -619,7 +638,8 @@ video:-webkit-full-screen {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-box {
|
.description-box,
|
||||||
|
.comments-section {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: var(--highlight-bg);
|
background-color: var(--highlight-bg);
|
||||||
@ -640,11 +660,16 @@ video:-webkit-full-screen {
|
|||||||
|
|
||||||
.info-box-item {
|
.info-box-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: var(--highlight-bg);
|
background-color: var(--highlight-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box-item p {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.description-text {
|
.description-text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -745,6 +770,22 @@ video:-webkit-full-screen {
|
|||||||
/* video page */
|
/* video page */
|
||||||
.video-main {
|
.video-main {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
position: relative; /* needed for modal */
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-modal {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 20%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-modal-text {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #eeeeee;
|
||||||
|
font-size: 1.3em;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-main video {
|
.video-main video {
|
||||||
@ -762,10 +803,26 @@ video:-webkit-full-screen {
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumb-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-tag-box {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-tag {
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid var(--accent-font-light);
|
||||||
|
}
|
||||||
|
|
||||||
.thumb-icon img,
|
.thumb-icon img,
|
||||||
.rating-stars img {
|
.rating-stars img {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin: 0;
|
margin: 0 5px;
|
||||||
filter: var(--img-filter);
|
filter: var(--img-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -803,6 +860,44 @@ video:-webkit-full-screen {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-box {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-box h3 {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-replies {
|
||||||
|
display: none;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 1px solid var(--accent-font-light);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-highlight {
|
||||||
|
background-color: var(--main-font);
|
||||||
|
padding: 3px;
|
||||||
|
color: var(--accent-font-dark);
|
||||||
|
font-family: Sen-bold, sans-serif;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-meta {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-carrot {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-like img {
|
||||||
|
width: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
filter: var(--img-filter-error);
|
||||||
|
}
|
||||||
|
|
||||||
/* multi search page */
|
/* multi search page */
|
||||||
.multi-search-box {
|
.multi-search-box {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
@ -812,10 +907,25 @@ video:-webkit-full-screen {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-search-result {
|
.multi-search-result,
|
||||||
|
#multi-search-results-placeholder {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#multi-search-results-placeholder span {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--accent-font-dark);
|
||||||
|
background-color: var(--highlight-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#multi-search-results-placeholder span.value {
|
||||||
|
color: var(--accent-font-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
#multi-search-results-placeholder ul {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* channel overview page */
|
/* channel overview page */
|
||||||
.channel-list.list {
|
.channel-list.list {
|
||||||
display: block;
|
display: block;
|
||||||
@ -923,13 +1033,12 @@ video:-webkit-full-screen {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dl-control-icons {
|
.task-control-icons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dl-control-icons img {
|
.task-control-icons img {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
@ -950,6 +1059,7 @@ video:-webkit-full-screen {
|
|||||||
|
|
||||||
/* status message */
|
/* status message */
|
||||||
.notification {
|
.notification {
|
||||||
|
position: relative;
|
||||||
background-color: var(--highlight-bg);
|
background-color: var(--highlight-bg);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 30px 0 15px 0;
|
padding: 30px 0 15px 0;
|
||||||
@ -1166,6 +1276,10 @@ video:-webkit-full-screen {
|
|||||||
margin: 15px;
|
margin: 15px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.view-controls.three {
|
||||||
|
grid-template-columns: unset;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
.sort {
|
.sort {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
81
package.json
81
package.json
@ -4,14 +4,12 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "remix build",
|
"build": "remix build",
|
||||||
"bun:build": "bun run node_modules/@remix-run/dev/dist/cli.js build",
|
|
||||||
"docker:build": "docker build -t remix .",
|
"docker:build": "docker build -t remix .",
|
||||||
"dev": "remix dev",
|
"dev": "remix dev",
|
||||||
"dev:remix": "cross-env NODE_ENV=development binode --require ./mocks -- @remix-run/dev:remix dev",
|
"dev:remix": "cross-env NODE_ENV=development binode --require ./mocks -- @remix-run/dev:remix dev",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"start": "remix-serve build",
|
"start": "remix-serve build",
|
||||||
"bun:start": "bun run ./server.ts",
|
|
||||||
"start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve build",
|
"start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve build",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"",
|
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"",
|
||||||
@ -29,61 +27,64 @@
|
|||||||
"/public/build"
|
"/public/build"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/node": "^1.7.2",
|
"@remix-run/node": "^1.15.0",
|
||||||
"@remix-run/react": "^1.7.2",
|
"@remix-run/react": "^1.15.0",
|
||||||
"@remix-run/serve": "^1.7.2",
|
"@remix-run/serve": "^1.15.0",
|
||||||
"@remix-run/server-runtime": "^1.7.2",
|
"@remix-run/server-runtime": "^1.15.0",
|
||||||
"isbot": "^3.6.1",
|
"isbot": "^3.6.10",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-player": "^2.11.0",
|
"react-player": "^2.12.0",
|
||||||
"tiny-invariant": "^1.2.0"
|
"tiny-invariant": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.5.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"@remix-run/dev": "^1.7.2",
|
"@remix-run/dev": "^1.15.0",
|
||||||
"@remix-run/eslint-config": "^1.7.2",
|
"@remix-run/eslint-config": "^1.15.0",
|
||||||
"@testing-library/cypress": "^8.0.3",
|
"@testing-library/cypress": "^9.0.0",
|
||||||
"@testing-library/dom": "^8.18.1",
|
"@testing-library/dom": "^9.2.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/eslint": "^8.4.6",
|
"@types/eslint": "^8.37.0",
|
||||||
"@types/node": "^18.7.18",
|
"@types/node": "^18.16.3",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.2.1",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"@vitest/coverage-c8": "^0.23.4",
|
"@vitest/coverage-c8": "^0.30.1",
|
||||||
"autoprefixer": "^10.4.11",
|
"autoprefixer": "^10.4.14",
|
||||||
"binode": "^1.0.5",
|
"binode": "^1.0.5",
|
||||||
"bun-types": "^0.2.0",
|
"c8": "^7.13.0",
|
||||||
"c8": "^7.12.0",
|
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^10.8.0",
|
"cypress": "^12.11.0",
|
||||||
"eslint": "^8.23.1",
|
"eslint": "^8.39.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "^2.13.3",
|
||||||
"happy-dom": "^6.0.4",
|
"happy-dom": "^9.10.1",
|
||||||
"msw": "^0.47.3",
|
"msw": "^1.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.23",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||||
"start-server-and-test": "^1.14.0",
|
"start-server-and-test": "^2.0.0",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.3.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths": "^4.1.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"vite": "^3.1.3",
|
"vite": "^4.3.3",
|
||||||
"vite-tsconfig-paths": "^3.5.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"vitest": "^0.23.4"
|
"vitest": "^0.30.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node --require tsconfig-paths/register prisma/seed.ts"
|
"seed": "ts-node --require tsconfig-paths/register prisma/seed.ts"
|
||||||
|
},
|
||||||
|
"volta": {
|
||||||
|
"node": "18.16.0",
|
||||||
|
"pnpm": "8.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10202
pnpm-lock.yaml
Normal file
10202
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,4 +4,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
cacheDirectory: "./node_modules/.cache/remix",
|
cacheDirectory: "./node_modules/.cache/remix",
|
||||||
ignoredRouteFiles: ["**/.*", "**/*.css", "**/*.test.{js,jsx,ts,tsx}"],
|
ignoredRouteFiles: ["**/.*", "**/*.css", "**/*.test.{js,jsx,ts,tsx}"],
|
||||||
|
future: {
|
||||||
|
v2_routeConvention: true,
|
||||||
|
v2_normalizeFormMethod: true,
|
||||||
|
v2_meta: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
70
server.ts
70
server.ts
@ -1,70 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import { createRequestHandler } from "@remix-run/server-runtime";
|
|
||||||
import * as build from "./build";
|
|
||||||
|
|
||||||
const mode = process.argv[2] === "dev" ? "development" : "production";
|
|
||||||
|
|
||||||
let requestHandler = createRequestHandler(build, mode);
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
Bun.gc(true);
|
|
||||||
}, 9000);
|
|
||||||
|
|
||||||
async function handler(request: Request): Promise<Response> {
|
|
||||||
if (mode === "development") {
|
|
||||||
let newBuild = await import("./build"); // <- This is the segfault source
|
|
||||||
requestHandler = createRequestHandler(newBuild, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = tryServeStaticFile("public", request);
|
|
||||||
if (file) return file;
|
|
||||||
|
|
||||||
return requestHandler(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = Bun.serve({
|
|
||||||
port: 3000,
|
|
||||||
fetch: mode === "development" ? liveReload(handler) : handler,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Server started at ${server.hostname}`);
|
|
||||||
|
|
||||||
function tryServeStaticFile(staticDir: string, request: Request): Response | undefined {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
if (url.pathname.length < 2) return undefined;
|
|
||||||
|
|
||||||
const filePath = path.join(staticDir, url.pathname);
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const file = Bun.file(filePath);
|
|
||||||
return new Response(file, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": file.type,
|
|
||||||
"Cache-Control": "public, max-age=31536000",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function liveReload<TFunc extends Function>(callback: TFunc) {
|
|
||||||
const registry = new Map([...Loader.registry.entries()]);
|
|
||||||
function reload() {
|
|
||||||
if (Loader.registry.size !== registry.size) {
|
|
||||||
for (const key of Loader.registry.keys()) {
|
|
||||||
if (!registry.has(key)) {
|
|
||||||
Loader.registry.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (...args: unknown[]) => {
|
|
||||||
reload();
|
|
||||||
return callback(...args);
|
|
||||||
};
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user