mirror of
https://github.com/tubearchivist/tubearchivist-frontend.git
synced 2024-11-24 12:50:11 +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;
|
||||
};
|
||||
|
||||
export const Layout: React.FC<Props> = ({ children }) => {
|
||||
export const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ minHeight: "100vh" }} className="main-content">
|
||||
<div className="main-content">
|
||||
<Nav />
|
||||
{children}
|
||||
</div>
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { useState } from "react";
|
||||
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 type { Datum } from "~/types/Videos";
|
||||
|
||||
type ViewStyle = "grid grid-3" | "list";
|
||||
|
||||
const VideoList = ({ videos }: { videos: Videos }) => {
|
||||
const VideoList = ({ videos }: { videos: Datum[] }) => {
|
||||
const [selectedVideoUrl, setSelectedVideoUrl] = useState<Datum>();
|
||||
const [viewStyle, setViewStyle] = useState<ViewStyle>("grid grid-3");
|
||||
|
||||
@ -22,7 +21,7 @@ const VideoList = ({ videos }: { videos: Videos }) => {
|
||||
setViewStyle(selectedViewStyle);
|
||||
};
|
||||
|
||||
if (videos.length < 1) {
|
||||
if (!videos) {
|
||||
return (
|
||||
<div className="boxed-content">
|
||||
<h2>No videos found...</h2>
|
||||
@ -110,7 +109,7 @@ const VideoList = ({ videos }: { videos: Videos }) => {
|
||||
<div style={{ cursor: "pointer" }} onClick={() => handleSelectedVideo(video)}>
|
||||
<div className={`video-thumb-wrap ${viewStyle}`}>
|
||||
<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 %} */}
|
||||
<div
|
||||
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 =
|
||||
process.env.API_KEY || `1954c006f731df60bf4c9f027d6b7076d699d319`;
|
||||
export const API_URL = process.env.API_URL || `https://tube.stiforr.tech`;
|
||||
export const API_KEY = process.env.API_KEY;
|
||||
export const API_URL = process.env.API_URL;
|
||||
|
@ -18,13 +18,10 @@ export const getChannels = async (token: string): Promise<Channels> => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Error getting channel information");
|
||||
}
|
||||
return response.json();
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const getChannel = async (
|
||||
token: string,
|
||||
id: string | undefined
|
||||
): Promise<Channel> => {
|
||||
export const getChannel = async (token: string, id: string | undefined): Promise<Channel> => {
|
||||
if (!token) {
|
||||
throw new Error(`Unable to fetch channels, no token provided`);
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import type { Datum, Videos } from "~/types/Videos";
|
||||
import { API_URL } from "./constants.server";
|
||||
import { getSession } from "~/session.server";
|
||||
|
||||
export const getVideos = async (token: string): Promise<Videos> => {
|
||||
if (!token) {
|
||||
throw new Error("Missing API token in request to get videos");
|
||||
}
|
||||
export const getVideos = async (request: Request) => {
|
||||
// if (!token) {
|
||||
// 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/`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
@ -15,10 +18,15 @@ export const getVideos = async (token: string): Promise<Videos> => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// if (response.status === 403) {
|
||||
// return redirect("/login");
|
||||
// }
|
||||
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> => {
|
||||
|
@ -10,11 +10,10 @@ import {
|
||||
} from "@remix-run/react";
|
||||
import { Layout } from "./components/Layout";
|
||||
import styles from "./styles/dark.css";
|
||||
import global from "./styles/globals.css";
|
||||
import global from "./styles/style.css";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
// { rel: "stylesheet", href: tailwindStylesheetUrl },
|
||||
{ rel: "stylesheet", href: styles },
|
||||
{ rel: "stylesheet", href: global },
|
||||
];
|
||||
@ -35,14 +34,14 @@ export async function loader({ request }: LoaderArgs) {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const data = useLoaderData();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
return (
|
||||
<html lang="en" className="h-full">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="h-full">
|
||||
<body>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</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 { getChannels } from "~/lib/getChannels";
|
||||
import type { Channels } from "~/types/channels";
|
||||
import { formatNumbers } from "../../lib/utils";
|
||||
import { formatNumbers } from "../lib/utils";
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
const channels = await getChannels(API_KEY);
|
||||
@ -93,10 +93,7 @@ const ChannelsPage = () => {
|
||||
) : (
|
||||
channels.data?.map((channel) => {
|
||||
return (
|
||||
<div
|
||||
key={channel.channel_id}
|
||||
className={`channel-item ${viewStyle}`}
|
||||
>
|
||||
<div key={channel.channel_id} className={`channel-item ${viewStyle}`}>
|
||||
<div className={`channel-banner ${viewStyle}`}>
|
||||
<Link to={channel.channel_id}>
|
||||
<img
|
||||
@ -109,22 +106,15 @@ const ChannelsPage = () => {
|
||||
<div className="info-box-item">
|
||||
<div className="round-img">
|
||||
<Link to={channel.channel_id}>
|
||||
<img
|
||||
src={`${API_URL}${channel.channel_thumb_url}`}
|
||||
alt="channel-thumb"
|
||||
/>
|
||||
<img src={`${API_URL}${channel.channel_thumb_url}`} alt="channel-thumb" />
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h3>
|
||||
<Link to={channel.channel_id}>
|
||||
{channel?.channel_name}
|
||||
</Link>
|
||||
<Link to={channel.channel_id}>{channel?.channel_name}</Link>
|
||||
</h3>
|
||||
{/* {% if channel.source.channel_subs >= 1000000 %} */}
|
||||
<p>
|
||||
Subscribers: {formatNumbers(channel?.channel_subs)}
|
||||
</p>
|
||||
<p>Subscribers: {formatNumbers(channel?.channel_subs)}</p>
|
||||
{/* {% else %} */}
|
||||
</div>
|
||||
</div>
|
||||
@ -136,20 +126,12 @@ const ChannelsPage = () => {
|
||||
className="unsubscribe"
|
||||
type="button"
|
||||
id="{{ channel.source.channel_id }}"
|
||||
onClick={() =>
|
||||
console.log(
|
||||
"unsubscribe(this.id) -> toggleSubscribe()"
|
||||
)
|
||||
}
|
||||
onClick={() => console.log("unsubscribe(this.id) -> toggleSubscribe()")}
|
||||
title={`${
|
||||
channel?.channel_subscribed
|
||||
? "Unsubscribe from"
|
||||
: "Subscribe to"
|
||||
channel?.channel_subscribed ? "Unsubscribe from" : "Subscribe to"
|
||||
} ${channel?.channel_name}`}
|
||||
>
|
||||
{channel?.channel_subscribed
|
||||
? "Unsubscribe"
|
||||
: "Subscribe"}
|
||||
{channel?.channel_subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</button>
|
||||
{/* {% else %} */}
|
||||
{/* <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 { useState } from "react";
|
||||
import IconAdd from "~/images/icon-add.svg";
|
||||
import IconGridView from "~/images/icon-gridview.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_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);
|
||||
|
||||
return playlists;
|
||||
@ -97,10 +96,7 @@ const Playlist = () => {
|
||||
) : (
|
||||
playlists.data.map((playlist) => {
|
||||
return (
|
||||
<div
|
||||
key={playlist.playlist_id}
|
||||
className={`playlist-item ${viewStyle}`}
|
||||
>
|
||||
<div key={playlist.playlist_id} className={`playlist-item ${viewStyle}`}>
|
||||
<div className="playlist-thumbnail">
|
||||
<Link to={playlist.playlist_id}>
|
||||
<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 type { User } from "~/models/user.server";
|
||||
import { getUserById } from "~/models/user.server";
|
||||
|
||||
import { API_URL } from "./lib/constants.server";
|
||||
import { csrfCookie } from "./cookies";
|
||||
invariant(process.env.SESSION_SECRET, "SESSION_SECRET must be set");
|
||||
|
||||
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";
|
||||
|
||||
export async function getSession(request: Request) {
|
||||
@ -24,61 +34,121 @@ export async function getSession(request: Request) {
|
||||
return sessionStorage.getSession(cookie);
|
||||
}
|
||||
|
||||
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 const getCsrfSession = async (request: Request) => {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
return csrfStorage.getSession(cookie);
|
||||
};
|
||||
|
||||
export async function getUser(request: Request) {
|
||||
const userId = await getUserId(request);
|
||||
if (userId === undefined) return null;
|
||||
export const createCsrfToken = async (request: Request) => {
|
||||
const cookieHeaders = request.headers.get("Cookie");
|
||||
const cookie = await csrfCookie.parse(cookieHeaders);
|
||||
|
||||
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}`);
|
||||
if (cookie) {
|
||||
return cookie;
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
export async function requireUser(request: Request) {
|
||||
const userId = await requireUserId(request);
|
||||
const token = randomBytes(100).toString("base64");
|
||||
return token;
|
||||
};
|
||||
|
||||
const user = await getUserById(userId);
|
||||
if (user) return user;
|
||||
type LoginResponse = {
|
||||
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({
|
||||
request,
|
||||
userId,
|
||||
remember,
|
||||
token,
|
||||
remember = false,
|
||||
redirectTo,
|
||||
}: {
|
||||
request: Request;
|
||||
userId: string;
|
||||
remember: boolean;
|
||||
token: string;
|
||||
remember?: boolean;
|
||||
redirectTo: string;
|
||||
}) {
|
||||
const session = await getSession(request);
|
||||
session.set(USER_SESSION_KEY, userId);
|
||||
// const session = await getSession(request);
|
||||
const csrf = await getCsrfSession(request);
|
||||
|
||||
// csrf.set(USER_SESSION_KEY, userId);
|
||||
// csrf.set("token", token);
|
||||
csrf.set("csrftoken", token);
|
||||
|
||||
return redirect(redirectTo, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session, {
|
||||
"Set-Cookie": await csrfStorage.commitSession(csrf, {
|
||||
maxAge: remember
|
||||
? 60 * 60 * 24 * 7 // 7 days
|
||||
: undefined,
|
||||
@ -88,10 +158,10 @@ export async function createUserSession({
|
||||
}
|
||||
|
||||
export async function logout(request: Request) {
|
||||
const session = await getSession(request);
|
||||
return redirect("/", {
|
||||
const session = await getCsrfSession(request);
|
||||
return redirect("/login", {
|
||||
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);
|
||||
}
|
||||
|
||||
.button-box {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.unsubscribe {
|
||||
background-color: var(--accent-font-light);
|
||||
}
|
||||
@ -349,6 +353,7 @@ button:hover {
|
||||
.grid-count {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-icons img {
|
||||
@ -369,6 +374,10 @@ button:hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#hidden-form button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#text-reveal {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
@ -391,6 +400,11 @@ button:hover {
|
||||
display: grid;
|
||||
align-content: space-evenly;
|
||||
height: 100vh;
|
||||
position: relative; /* needed for modal */
|
||||
}
|
||||
|
||||
#notifications {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
@ -460,7 +474,7 @@ video:-webkit-full-screen {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-item:hover .video-thumb span {
|
||||
.video-item:hover .video-tags {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -471,7 +485,8 @@ video:-webkit-full-screen {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-progress-bar {
|
||||
.video-progress-bar,
|
||||
.notification-progress-bar {
|
||||
position: absolute;
|
||||
background-color: var(--accent-font-dark);
|
||||
height: 7px;
|
||||
@ -484,16 +499,20 @@ video:-webkit-full-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-thumb span {
|
||||
.video-tags {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
background-color: var(--accent-font-light);
|
||||
left: 0;
|
||||
padding: 5px;
|
||||
opacity: 0;
|
||||
transition: 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.video-tags span {
|
||||
background-color: var(--accent-font-light);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.video-play img {
|
||||
width: 40px;
|
||||
filter: var(--img-filter);
|
||||
@ -619,7 +638,8 @@ video:-webkit-full-screen {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.description-box {
|
||||
.description-box,
|
||||
.comments-section {
|
||||
margin-top: 1rem;
|
||||
padding: 15px;
|
||||
background-color: var(--highlight-bg);
|
||||
@ -640,11 +660,16 @@ video:-webkit-full-screen {
|
||||
|
||||
.info-box-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background-color: var(--highlight-bg);
|
||||
}
|
||||
|
||||
.info-box-item p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
width: 100%;
|
||||
}
|
||||
@ -745,6 +770,22 @@ video:-webkit-full-screen {
|
||||
/* video page */
|
||||
.video-main {
|
||||
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 {
|
||||
@ -762,10 +803,26 @@ video:-webkit-full-screen {
|
||||
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,
|
||||
.rating-stars img {
|
||||
width: 20px;
|
||||
margin: 0;
|
||||
margin: 0 5px;
|
||||
filter: var(--img-filter);
|
||||
}
|
||||
|
||||
@ -803,6 +860,44 @@ video:-webkit-full-screen {
|
||||
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-box {
|
||||
padding-right: 20px;
|
||||
@ -812,10 +907,25 @@ video:-webkit-full-screen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-search-result {
|
||||
.multi-search-result,
|
||||
#multi-search-results-placeholder {
|
||||
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-list.list {
|
||||
display: block;
|
||||
@ -923,13 +1033,12 @@ video:-webkit-full-screen {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dl-control-icons {
|
||||
.task-control-icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dl-control-icons img {
|
||||
.task-control-icons img {
|
||||
width: 30px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
@ -950,6 +1059,7 @@ video:-webkit-full-screen {
|
||||
|
||||
/* status message */
|
||||
.notification {
|
||||
position: relative;
|
||||
background-color: var(--highlight-bg);
|
||||
text-align: center;
|
||||
padding: 30px 0 15px 0;
|
||||
@ -1166,6 +1276,10 @@ video:-webkit-full-screen {
|
||||
margin: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.view-controls.three {
|
||||
grid-template-columns: unset;
|
||||
justify-content: center;
|
||||
}
|
||||
.sort {
|
||||
display: block;
|
||||
}
|
81
package.json
81
package.json
@ -4,14 +4,12 @@
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"bun:build": "bun run node_modules/@remix-run/dev/dist/cli.js build",
|
||||
"docker:build": "docker build -t remix .",
|
||||
"dev": "remix dev",
|
||||
"dev:remix": "cross-env NODE_ENV=development binode --require ./mocks -- @remix-run/dev:remix dev",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "remix-serve build",
|
||||
"bun:start": "bun run ./server.ts",
|
||||
"start:mocks": "binode --require ./mocks -- @remix-run/serve:remix-serve build",
|
||||
"test": "vitest",
|
||||
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"",
|
||||
@ -29,61 +27,64 @@
|
||||
"/public/build"
|
||||
],
|
||||
"dependencies": {
|
||||
"@remix-run/node": "^1.7.2",
|
||||
"@remix-run/react": "^1.7.2",
|
||||
"@remix-run/serve": "^1.7.2",
|
||||
"@remix-run/server-runtime": "^1.7.2",
|
||||
"isbot": "^3.6.1",
|
||||
"@remix-run/node": "^1.15.0",
|
||||
"@remix-run/react": "^1.15.0",
|
||||
"@remix-run/serve": "^1.15.0",
|
||||
"@remix-run/server-runtime": "^1.15.0",
|
||||
"isbot": "^3.6.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-player": "^2.11.0",
|
||||
"tiny-invariant": "^1.2.0"
|
||||
"react-player": "^2.12.0",
|
||||
"tiny-invariant": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@remix-run/dev": "^1.7.2",
|
||||
"@remix-run/eslint-config": "^1.7.2",
|
||||
"@testing-library/cypress": "^8.0.3",
|
||||
"@testing-library/dom": "^8.18.1",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@remix-run/dev": "^1.15.0",
|
||||
"@remix-run/eslint-config": "^1.15.0",
|
||||
"@testing-library/cypress": "^9.0.0",
|
||||
"@testing-library/dom": "^9.2.0",
|
||||
"@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",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/eslint": "^8.4.6",
|
||||
"@types/node": "^18.7.18",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"@vitest/coverage-c8": "^0.23.4",
|
||||
"autoprefixer": "^10.4.11",
|
||||
"@types/eslint": "^8.37.0",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitest/coverage-c8": "^0.30.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"binode": "^1.0.5",
|
||||
"bun-types": "^0.2.0",
|
||||
"c8": "^7.12.0",
|
||||
"c8": "^7.13.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.8.0",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"happy-dom": "^6.0.4",
|
||||
"msw": "^0.47.3",
|
||||
"cypress": "^12.11.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-cypress": "^2.13.3",
|
||||
"happy-dom": "^9.10.1",
|
||||
"msw": "^1.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "2.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^4.8.3",
|
||||
"vite": "^3.1.3",
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^0.23.4"
|
||||
"vite": "^4.3.3",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.30.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
},
|
||||
"prisma": {
|
||||
"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 = {
|
||||
cacheDirectory: "./node_modules/.cache/remix",
|
||||
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