Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
chrcit committed Nov 5, 2024
0 parents commit 0f8bf32
Show file tree
Hide file tree
Showing 73 changed files with 20,886 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
TURSO_URL=
TURSO_TOKEN=
DATABASE_URL=

YOUTUBE_API_KEY=

TWITCH_CLIENT_ID=
TWITCH_CLIENT_SECRET=
TWITCH_ACCESS_TOKEN=

TOP_OF_THE_HOUR_SECRET=
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"]
}
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules

.cache
.env
.vercel
.output

/build/
/public/build
/api

.DS_Store
.pscale.yml
Empty file added .prettierrc
Empty file.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Hasanhub

## Refresh access token

```bash
curl -X POST 'https://id.twitch.tv/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=ttsgjnui5mqji4aqjvzahh3mrlyzy3&client_secret=[CLIENT_SECRET]&grant_type=client_credentials'
```
22 changes: 22 additions & 0 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";
// import { useLocation, useMatches } from "@remix-run/react";
// import * as Sentry from "@sentry/remix";
// import { useEffect } from "react";

// Sentry.init({
// dsn: "https://[email protected]/6564125",
// tracesSampleRate: 1,
// integrations: [
// new Sentry.BrowserTracing({
// routingInstrumentation: Sentry.remixRouterInstrumentation(
// useEffect,
// useLocation,
// useMatches
// ),
// }),
// ],
// // ...
// });

hydrate(<RemixBrowser />, document);
29 changes: 29 additions & 0 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
// import * as Sentry from "@sentry/remix";
// import { prisma } from "~/utils/prisma.server";

// Sentry.init({
// dsn: "https://[email protected]/6564125",
// tracesSampleRate: 1,
// integrations: [new Sentry.Integrations.Prisma({ client: prisma })],
// });

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}
55 changes: 55 additions & 0 deletions app/hooks/use-action-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import useUrlState from "~/hooks/use-url-state";
import type { DurationType } from "~/utils/validators";
import type { OrderByType, OrderDirectionType } from "../utils/validators";

const useActionUrl = () => {
const current = useUrlState();

const constructUrl = (
action: {
tagSlugs?: string[];
durations?: DurationType[];
ordering?: { by?: OrderByType; order?: OrderDirectionType };
lastVideoId?: number;
},
index = false
) => {
let merged = {
...current,
...action,
};
merged.tagSlugs = merged?.tagSlugs?.filter(Boolean);
merged.durations = merged?.durations?.filter(Boolean);

const basePath =
merged.tagSlugs.length > 0
? `/tags/${merged.tagSlugs.join("/")}?`
: index
? "?index&"
: "/?";

const searchParams = new URLSearchParams();

merged.durations?.forEach((duration: DurationType) => {
searchParams.append("durations", duration);
});

if (merged.ordering.order && merged.ordering.order !== "desc") {
searchParams.append("order", merged.ordering.order);
}

if (merged.ordering.by && merged.ordering.by !== "publishedAt") {
searchParams.append("by", merged.ordering.by);
}

if (merged.lastVideoId) {
searchParams.append("lastVideoId", merged.lastVideoId.toString());
}

return `${basePath}${searchParams.toString()}`;
};

return { current, constructUrl };
};

export default useActionUrl;
94 changes: 94 additions & 0 deletions app/hooks/use-url-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useLocation, useSearchParams, useTransition } from "@remix-run/react";
import { useEffect, useState } from "react";
import type {
LastVideoIdType,
DurationListType,
OrderByType,
OrderDirectionType,
} from "../utils/validators";
import { UrlParamsSchema, DurationListValidator } from "../utils/validators";

const getTagSlugsFromPathname = (location?: string | null) => {
if (location === null || location === undefined) {
return [];
}

return location.replace("/tags/", "").split("/");
};

type UrlStateType = {
tagSlugs: string[];
lastVideoId?: LastVideoIdType;
durations?: DurationListType;
ordering: {
by: OrderByType;
order: OrderDirectionType;
};
};

const useUrlState = () => {
const location = useLocation();
const [searchParams] = useSearchParams();

const [urlState, setUrlState] = useState<UrlStateType>({
tagSlugs: getTagSlugsFromPathname(location?.pathname),
durations:
DurationListValidator.parse(searchParams.getAll("durations")) ?? null,
ordering: {
order: "desc",
by: "publishedAt",
},
});

const transition = useTransition();

useEffect(() => {
const nextSearchParams = new URLSearchParams(transition.location?.search);

let lastVideoIdParam = searchParams.get("lastVideoId");
let nextLastVideoIdParam = nextSearchParams.get("lastVideoId");

const tagSlugs = getTagSlugsFromPathname(location?.pathname);
const nextTagSlugs = getTagSlugsFromPathname(
transition?.location?.pathname
);

const { order, durations, by, lastVideoId } = UrlParamsSchema.parse({
order: searchParams.get("order") ?? undefined,
durations: searchParams.getAll("durations"),
by: searchParams.get("by") ?? undefined,
lastVideoId: lastVideoIdParam ? parseInt(lastVideoIdParam) : undefined,
});

const {
order: nextOrder,
durations: nextDurations,
by: nextBy,
lastVideoId: nextLastVideoId,
} = UrlParamsSchema.parse({
order: nextSearchParams.get("order") ?? undefined,
durations: nextSearchParams.getAll("durations"),
by: nextSearchParams.get("by") ?? undefined,
lastVideoId: nextLastVideoIdParam
? parseInt(nextLastVideoIdParam)
: undefined,
});

setUrlState({
durations: nextDurations?.length !== 0 ? nextDurations : durations,
ordering: {
order: nextOrder ?? order ?? "desc",
by: nextBy ?? by ?? "publishedAt",
},
lastVideoId: nextLastVideoId ?? lastVideoId,
tagSlugs: nextTagSlugs.length !== 0 ? nextTagSlugs : tagSlugs,
});
}, [location, transition.location, searchParams]);

return {
isLoading: transition.state === "loading",
...urlState,
};
};

export default useUrlState;
17 changes: 17 additions & 0 deletions app/lib/get-active-tags-by-slugs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { prisma } from "~/utils/prisma.server";
const getActiveTagsBySlugs = async (tagSlugs: string[] | undefined) => {
return tagSlugs
? await prisma.tag.findMany({
where: {
slug: { in: tagSlugs },
},
select: {
id: true,
name: true,
slug: true,
},
})
: [];
};

export default getActiveTagsBySlugs;
60 changes: 60 additions & 0 deletions app/lib/get-stream-info.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export type StreamInfo = {
data: {
id: string;
user_id: string;
user_login: string;
user_name: string;
game_id: string;
game_name: string;
type: string;
title: string;
viewer_count: number;
started_at: Date;
language: string;
thumbnail_url: string;
tag_ids: string[];
is_mature: boolean;
}[];
pagination: {
cursor: string;
};
};

export type StreamSchedule = {
data: {
segments: {
id: string;
start_time: Date;
end_time: Date;
title: string;
canceled_until?: any;
category: {
id: string;
name: string;
};
is_recurring: boolean;
}[];
broadcaster_id: string;
broadcaster_name: string;
broadcaster_login: string;
vacation?: any;
};
pagination: {};
};

export const getStreamInfo = async () => {
return await Promise.all([
fetch(`https://api.twitch.tv/helix/streams?first=1&user_id=${207813352}`, {
headers: {
"Client-Id": process.env.TWITCH_CLIENT_ID?.trim() ?? "",
Authorization: `Bearer ${process.env.TWITCH_ACCESS_TOKEN ?? ""}`,
},
}).then((res) => res.json()) as unknown as StreamInfo,
fetch(`https://api.twitch.tv/helix/schedule?broadcaster_id=${207813352}`, {
headers: {
"Client-Id": process.env.TWITCH_CLIENT_ID?.trim() ?? "",
Authorization: `Bearer ${process.env.TWITCH_ACCESS_TOKEN ?? ""}`,
},
}).then((res) => res.json()) as unknown as StreamSchedule,
]);
};
Loading

0 comments on commit 0f8bf32

Please sign in to comment.