├── .eslintrc.json ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── mdx-components.tsx ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── background.svg ├── favicon.ico ├── favicon.svg ├── marker-light.svg ├── marker.svg ├── next.svg ├── robots.txt └── vercel.svg ├── src ├── .env.example ├── app │ ├── (core) │ │ ├── about │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── v │ │ │ └── [slug] │ │ │ ├── page.tsx │ │ │ └── player.tsx │ ├── [slug] │ │ ├── layout.tsx │ │ └── page.tsx │ ├── _components │ │ ├── about.mdx │ │ ├── common │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ └── text-input.tsx │ │ ├── error-page.tsx │ │ ├── gotovods.tsx │ │ ├── icon.tsx │ │ ├── layout-helper.tsx │ │ ├── logomark.tsx │ │ ├── signin.tsx │ │ └── vods.tsx │ ├── error.tsx │ ├── globals.css │ ├── layout.tsx │ └── sitemap.ts ├── assets │ ├── background.svg │ └── puff.svg ├── components │ └── loading.tsx ├── middleware.ts └── utils │ ├── classnames.ts │ ├── twitch-server.ts │ └── types │ └── twitch-player.d.ts ├── tailwind.config.js ├── tailwind.typography.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | DATABASE_URL: "https://fake.com" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Install Dependencies 17 | run: npm install -g pnpm && pnpm install 18 | 19 | - name: Typecheck 20 | run: pnpm typecheck 21 | 22 | - name: Lint 23 | run: pnpm lint 24 | 25 | - name: Print Environment Variable 26 | run: echo $MY_ENV_VAR 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ping.gg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [MarkerThing](https://markerthing.com) (by [Ping](https://ping.gg)) 2 | 3 | tl;dr - grab csv files from your Twitch [Stream Markers](https://help.twitch.tv/s/article/creating-highlights-and-stream-markers?language=en_US#:~:text=in%20light%20purple.-,Stream%20Markers,-Stream%20Markers%20are) to use with [LosslessCut](https://github.com/mifi/lossless-cut) 4 | 5 | ## Dev Setup 6 | 7 | 1. Fork & clone repo 8 | 2. `pnpm install` 9 | 3. Set up [Twitch OAuth app](https://dev.twitch.tv) and [Clerk auth](https://clerk.com/?utm_campaign=theo-dtc) 10 | - You need to use the custom twitch oauth credentials in clerk 11 | - You also need to add the `user:read:broadcast` and `openid` scopes 12 | 4. Add the environment variables to a new `.env.local` file (see [.env.example](/src/.env.example)) 13 | 5. `pnpm dev` 14 | 15 | ## Tech used 16 | 17 | - [Next.js App Router](https://beta.nextjs.org) 18 | - [React Server Components](https://react.dev) 19 | - [Clerk auth](https://clerk.com/?utm_campaign=theo-dtc) 20 | - [Tailwind](https://tailwindcss.com) 21 | - [Plausible analytics](https://plausible.io/?ref=theo) 22 | -------------------------------------------------------------------------------- /mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import { MDXComponents } from "mdx/types"; 2 | 3 | export function useMDXComponents(components: MDXComponents): MDXComponents { 4 | return components; 5 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | mdxRs: true, 5 | }, 6 | images: { 7 | domains: ["static-cdn.jtvnw.net", "vod-secure.twitch.tv"], 8 | }, 9 | 10 | typescript: { 11 | ignoreBuildErrors: true, 12 | }, 13 | eslint: { 14 | ignoreDuringBuilds: true, 15 | }, 16 | }; 17 | 18 | const withMdx = require("@next/mdx")()(nextConfig); 19 | 20 | const { withPlausibleProxy } = require("next-plausible"); 21 | 22 | module.exports = withPlausibleProxy()(withMdx); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markerthing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">=18.0.0" 7 | }, 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@clerk/nextjs": "^5.2.6", 17 | "@clerk/themes": "^2.1.13", 18 | "@next/mdx": "^14.2.5", 19 | "@types/node": "18.15.11", 20 | "@types/react": "18.3.3", 21 | "@types/react-dom": "18.3.0", 22 | "autoprefixer": "10.4.14", 23 | "dayjs": "^1.11.7", 24 | "eslint": "8.37.0", 25 | "eslint-config-next": "14.2.5", 26 | "next": "14.2.5", 27 | "next-plausible": "^3.7.2", 28 | "postcss": "8.4.21", 29 | "react": "18.3.1", 30 | "react-dom": "18.3.1", 31 | "react-hot-toast": "^2.4.0", 32 | "tailwindcss": "3.3.1", 33 | "typescript": "5.0.3" 34 | }, 35 | "devDependencies": { 36 | "@tailwindcss/typography": "^0.5.9", 37 | "@types/mdx": "^2.0.4", 38 | "prettier": "^2.8.7", 39 | "prettier-plugin-tailwindcss": "^0.2.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // I only have this file because pnpm breaks prettier autoconfigs without it 2 | // See: https://github.com/pnpm/pnpm/issues/4700 3 | 4 | module.exports = { 5 | plugins: [require("prettier-plugin-tailwindcss")], 6 | }; 7 | -------------------------------------------------------------------------------- /public/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 27 | 29 | 30 | 32 | 34 | 36 | 37 | 39 | 40 | 42 | 43 | 45 | 47 | 49 | 50 | 52 | 54 | 56 | 58 | 60 | 61 | 63 | 65 | 67 | 69 | 71 | 72 | 74 | 76 | 78 | 80 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 94 | 96 | 98 | 99 | 101 | 103 | 105 | 107 | 109 | 111 | 113 | 114 | 116 | 117 | 119 | 121 | 123 | 125 | 127 | 129 | 131 | 133 | 135 | 136 | 138 | 139 | 141 | 142 | 144 | 146 | 148 | 149 | 151 | 152 | 154 | 156 | 158 | 160 | 162 | 163 | 165 | 166 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingdotgg/markerthing/c7a00ebaa9f7f1e6202a9a1d868187c8da40baf1/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/marker-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Sitemap: https://markerthing.com/sitemap.xml -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=[clerk_public_key] 2 | CLERK_SECRET_KEY=[clerk_secret_key] 3 | TWITCH_CLIENT_ID=[twitch_client_id] 4 | TWITCH_CLIENT_SECRET=[twitch_client_secret] -------------------------------------------------------------------------------- /src/app/(core)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import About from "~/app/_components/about.mdx"; 2 | 3 | export default async function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(core)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutHelper } from "../_components/layout-helper"; 2 | 3 | export default async function CoreLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | // @ts-expect-error Server Components :( 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(core)/page.tsx: -------------------------------------------------------------------------------- 1 | import About from "../_components/about.mdx"; 2 | 3 | export default async function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(core)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(core)/v/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server"; 2 | import { 3 | getTwitchTokenFromClerk, 4 | getVodWithMarkers, 5 | } from "~/utils/twitch-server"; 6 | import { VodPlayer } from "./player"; 7 | import Script from "next/script"; 8 | 9 | export const dynamic = "force-dynamic"; 10 | 11 | // I do the revalidate 0 here because "force-dynamic" doesn't actually work 12 | // See: https://github.com/vercel/next.js/issues/47273 13 | export const revalidate = 60; 14 | 15 | export default async function VodPage({ 16 | params, 17 | }: { 18 | params: { slug: string }; 19 | }) { 20 | const self = await auth(); 21 | if (!self || !self.userId) return
You have to be signed in
; 22 | 23 | const token = await getTwitchTokenFromClerk(self.userId); 24 | 25 | const vodDetails = await getVodWithMarkers(params.slug, token); 26 | 27 | return ( 28 | <> 29 |