mirror of
https://github.com/mit-regressions/viewer.git
synced 2025-04-09 14:20:15 -04:00
Merge pull request #4 from mit-regressions/igel-t3-initial
Merge initial create-t3-app prototype
This commit is contained in:
commit
0136cdffa6
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -134,4 +134,6 @@ GitHub.sublime-settings
|
|||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history
|
||||
.history
|
||||
.DS_STORE
|
||||
.vercel
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 MIT: REGRESSIONS
|
||||
Copyright (c) 2022 REGRESSIONS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
12
Pipfile
12
Pipfile
|
@ -1,12 +0,0 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
75
Pipfile.lock
generated
75
Pipfile.lock
generated
|
@ -1,75 +0,0 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "99c4b9ec1b8891ff787677276760beb6d6d4919c55660da1c713682156a6086c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.8"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4",
|
||||
"sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.5.2"
|
||||
},
|
||||
"backports.zoneinfo": {
|
||||
"hashes": [
|
||||
"sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf",
|
||||
"sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328",
|
||||
"sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546",
|
||||
"sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6",
|
||||
"sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570",
|
||||
"sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9",
|
||||
"sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7",
|
||||
"sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987",
|
||||
"sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722",
|
||||
"sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582",
|
||||
"sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc",
|
||||
"sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b",
|
||||
"sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1",
|
||||
"sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08",
|
||||
"sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac",
|
||||
"sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"
|
||||
],
|
||||
"markers": "python_version < '3.9'",
|
||||
"version": "==0.2.1"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1",
|
||||
"sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.1.3"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34",
|
||||
"sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"tzdata": {
|
||||
"hashes": [
|
||||
"sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d",
|
||||
"sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==2022.7"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
11
README.md
11
README.md
|
@ -1,2 +1,13 @@
|
|||
# viewer
|
||||
interactive player for MIT: REGRESSIONS, with footnotes, sources, and more
|
||||
|
||||
Currently using NextJS with create-t3-app and deploying to Vercel (https://vercel.com/regressions)
|
||||
|
||||
Uses react-player to play video in-browser, currently experimenting with [webvtt-player](https://github.com/umd-mith/webvtt-player) to display captions + sources + commentary + footnotes as the video plays!
|
||||
|
||||
TODO:
|
||||
- create custom VTT schema with [OHMS support]() for our project's needs (implies ample unit testing and some E2E)
|
||||
- create VTT builder so we can easily add sources (likely will be a light GUI, don't want to just do plain text editing of VTTs)
|
||||
- create pretty caption playback
|
||||
|
||||
View current deployment at https://viewer-dukeeagle-regressions.vercel.app/
|
22
manage.py
22
manage.py
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
20
viewer/.env.example
Normal file
20
viewer/.env.example
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
|
||||
# Keep this file up-to-date when you add new variables to `.env`.
|
||||
|
||||
# This file will be committed to version control, so make sure not to have any secrets in it.
|
||||
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
|
||||
|
||||
# When adding additional env variables, the schema in /env/schema.mjs should be updated accordingly
|
||||
|
||||
# Prisma
|
||||
DATABASE_URL=file:./db.sqlite
|
||||
|
||||
# Next Auth
|
||||
# You can generate the secret via 'openssl rand -base64 32' on Linux
|
||||
# More info: https://next-auth.js.org/configuration/options#secret
|
||||
# NEXTAUTH_SECRET=
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Next Auth Discord Provider
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
11
viewer/.eslintrc.json
Normal file
11
viewer/.eslintrc.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-imports": "warn"
|
||||
}
|
||||
}
|
42
viewer/.gitignore
vendored
Normal file
42
viewer/.gitignore
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
28
viewer/README.md
Normal file
28
viewer/README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Create T3 App
|
||||
|
||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
|
||||
|
||||
## What's next? How do I make an app with this?
|
||||
|
||||
We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
|
||||
|
||||
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
|
||||
|
||||
- [Next.js](https://nextjs.org)
|
||||
- [NextAuth.js](https://next-auth.js.org)
|
||||
- [Prisma](https://prisma.io)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [tRPC](https://trpc.io)
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
|
||||
|
||||
- [Documentation](https://create.t3.gg/)
|
||||
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
|
||||
|
||||
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
|
||||
|
||||
## How do I deploy this?
|
||||
|
||||
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PlayerConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "player"
|
|
@ -1,3 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,6 +0,0 @@
|
|||
from . import views
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.player),
|
||||
]
|
|
@ -1,5 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
def player(request):
|
||||
return render(request, "player/player.html")
|
|
@ -1,5 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from . import models
|
||||
# Register your models here.
|
||||
admin.site.register(models.Annotation)
|
||||
admin.site.register(models.Comment)
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "users"
|
|
@ -1,20 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your models here.
|
||||
class Annotation(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
timestamp = models.CharField(max_length=20)
|
||||
contents = models.CharField(max_length=1000)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.timestamp}"
|
||||
|
||||
class Comment(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
timestamp = models.CharField(max_length=20)
|
||||
contents = models.CharField(max_length=1000)
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.timestamp} @ {self.author}"
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,7 +0,0 @@
|
|||
from . import views
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path("login/", views.login, name="login"),
|
||||
path("signup/", views.signup, name="signup")
|
||||
]
|
|
@ -1,8 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
def signup(request):
|
||||
return render(request, "users/signup.html")
|
||||
|
||||
def login(request):
|
||||
return render(request, "users/login.html")
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
ASGI config for viewer project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
|
||||
|
||||
application = get_asgi_application()
|
25
viewer/next.config.mjs
Normal file
25
viewer/next.config.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||
* This is especially useful for Docker builds.
|
||||
*/
|
||||
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs"));
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
|
||||
// TEMPORARY. will be removed once ref and other TS errors are fixed
|
||||
typescript: {
|
||||
// ignore typescript errors
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
swcMinify: true,
|
||||
i18n: {
|
||||
locales: ["en"],
|
||||
defaultLocale: "en",
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
7466
viewer/package-lock.json
generated
Normal file
7466
viewer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
viewer/package.json
Normal file
50
viewer/package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "viewer",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@prisma/client": "^4.5.0",
|
||||
"@tanstack/react-query": "^4.16.0",
|
||||
"@trpc/client": "^10.0.0",
|
||||
"@trpc/next": "^10.0.0",
|
||||
"@trpc/react-query": "^10.0.0",
|
||||
"@trpc/server": "^10.0.0",
|
||||
"next": "13.1.1",
|
||||
"next-auth": "^4.18.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-player": "^2.11.0",
|
||||
"superjson": "1.9.1",
|
||||
"webvtt-player": "^0.0.16",
|
||||
"zod": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-next": "13.1.1",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.2.1",
|
||||
"prisma": "^4.5.0",
|
||||
"tailwindcss": "^3.2.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "6.11.6"
|
||||
}
|
||||
}
|
6
viewer/postcss.config.cjs
Normal file
6
viewer/postcss.config.cjs
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
4
viewer/prettier.config.cjs
Normal file
4
viewer/prettier.config.cjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
||||
};
|
66
viewer/prisma/schema.prisma
Normal file
66
viewer/prisma/schema.prisma
Normal file
|
@ -0,0 +1,66 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
// NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below
|
||||
// Further reading:
|
||||
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
|
||||
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Example {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
BIN
viewer/public/data/MIT Regressions intro audio.mp3
Normal file
BIN
viewer/public/data/MIT Regressions intro audio.mp3
Normal file
Binary file not shown.
40
viewer/public/data/MIT Regressions intro captions.vtt
Normal file
40
viewer/public/data/MIT Regressions intro captions.vtt
Normal file
|
@ -0,0 +1,40 @@
|
|||
WEBVTT
|
||||
Kind: captions
|
||||
Language: en
|
||||
|
||||
00:00:14.068 --> 00:00:16.837
|
||||
The Massachusetts Institute of Technology began with a promise.
|
||||
|
||||
00:00:19.252 --> 00:00:21.331
|
||||
A promise that with a devotion to technology,
|
||||
|
||||
00:00:22.008 --> 00:00:23.735
|
||||
through rigorous study of math and science,
|
||||
|
||||
00:00:23.939 --> 00:00:25.504
|
||||
we can engineer a better tomorrow.
|
||||
|
||||
00:00:26.603 --> 00:00:27.599
|
||||
we can make
|
||||
|
||||
00:00:27.599 --> 00:00:30.000
|
||||
a Better World.
|
||||
|
||||
00:00:34.095 --> 00:00:35.347
|
||||
In this story,
|
||||
|
||||
00:00:35.347 --> 00:00:37.242
|
||||
we ask a simple question.
|
||||
|
||||
00:00:39.045 --> 00:00:40.283
|
||||
Have we kept our promise?
|
||||
|
||||
00:01:07.526 --> 00:01:10.121
|
||||
MIT was founded in 1861.
|
||||
|
||||
00:01:10.121 --> 00:01:13.448
|
||||
But the Institute as we know it today was born in 1941.
|
||||
|
||||
00:01:13.448 --> 00:01:15.448
|
||||
December 7th, 1941.
|
||||
|
412
viewer/public/data/MIT Regressions intro metadata.vtt
Normal file
412
viewer/public/data/MIT Regressions intro metadata.vtt
Normal file
|
@ -0,0 +1,412 @@
|
|||
WEBVTT
|
||||
Kind: captions
|
||||
Language: en
|
||||
|
||||
NOTE separation between MUSIC SOURCE, VIDEO SOURCE, NARRATION SOURCE, COMMENTARY, and TRANSCRIPT.
|
||||
|
||||
00:00:01.550 --> 00:01:15.448
|
||||
{
|
||||
"uid": "1",
|
||||
"type": "music",
|
||||
"data": {
|
||||
"type": "music",
|
||||
"title": "The Vanishing American Family",
|
||||
"artist": "ScubaZ",
|
||||
"year": "2001",
|
||||
"label": "Odd Records Ltd.",
|
||||
"hyperlink": "https://www.discogs.com/master/1184747-Scuba-Z-The-Vanishing-American-Family"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:01.560 --> 00:00:04.948
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "original footage",
|
||||
"artist": "MIT: REGRESSIONS",
|
||||
"year": "2022",
|
||||
"notes": "",
|
||||
"retrieved_from": "",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:04.949 --> 00:00:12.948
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT Holiday Greeting 2021",
|
||||
"artist": "MIT Video Productions",
|
||||
"year": "2021",
|
||||
"notes": "",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=7X2r6863KGA"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:13.560 --> 00:00:15.559
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "original footage",
|
||||
"artist": "MIT: REGRESSIONS",
|
||||
"year": "2021",
|
||||
"notes": "",
|
||||
"retrieved_from": "",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:15.560 --> 00:00:17.549
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Fall around MIT",
|
||||
"artist": "Andrei Ivanov",
|
||||
"year": "2012",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=xZXTpEJJxgs"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:17.550 --> 00:00:19.548
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "\"We lived here, you didn't...\" @ Bexley Hall, MIT",
|
||||
"artist": "Andrei Ivanov",
|
||||
"year": "2013",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=e6m8se96yyM&ab_channel=AndreiIvanov"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:19.549 --> 00:00:24.200
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Course 3 at MIT (confirm)",
|
||||
"artist": "MIT Video Productions",
|
||||
"year": "2019",
|
||||
"notes": "",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=e6m8se96yyM&ab_channel=AndreiIvanov"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:24.201 --> 00:00:29.200
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT.nano: An Overview",
|
||||
"artist": "MIT",
|
||||
"year": "2015",
|
||||
"notes": "",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=rGnr2ipfY3o"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:29.201 --> 00:00:30.999
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "ZigZag Episode X",
|
||||
"artist": "MIT",
|
||||
"year": "200X",
|
||||
"notes": "AI-upscaled",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:31.000 --> 00:00:32.900
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "ZigZag Episode X2",
|
||||
"artist": "MIT",
|
||||
"year": "200X",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:33.000 --> 00:00:36.000
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "A Celebration of Charles Vest",
|
||||
"artist": "MIT Video Productions",
|
||||
"year": "2005",
|
||||
"notes": "AI-upscaled and color corrected",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:36.000 --> 00:00:38.100
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT Project Athena: A Computer-Aided Teaching System",
|
||||
"artist": "MIT Video Productions",
|
||||
"year": "1984",
|
||||
"notes": "AI-upscaled and color corrected",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=tG7i7HCD9g0&t=34s"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:38.101 --> 00:00:41.800
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Afgan: The Soviet Experience",
|
||||
"artist": "",
|
||||
"year": "1989",
|
||||
"notes": "AI-upscaled and color corrected",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=tG7i7HCD9g0&t=34s"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
00:00:41.801 --> 00:00:42.800
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Chemistry Lab at MIT",
|
||||
"artist": "WGBH",
|
||||
"year": "1980s",
|
||||
"notes": "AI-upscaled",
|
||||
"retrieved_from": "WGBH Open Vault",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:43.001 --> 00:00:44.000
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Jugglers at MIT",
|
||||
"artist": "(confirm)",
|
||||
"year": "1970s",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "YouTube",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:44.001 --> 00:00:45.600
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "The November Actions",
|
||||
"artist": "Ricky Leacock",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:45.601 --> 00:00:47.499
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT: Progressions",
|
||||
"artist": "David and Sheri Espar",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "Kenneth Friedman (YouTube)",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:47.500 --> 00:00:48.600
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "The November Actions",
|
||||
"artist": "Ricky Leacock",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:48.601 --> 00:00:50.900
|
||||
{
|
||||
"uid": "2",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT Centennial Procession",
|
||||
"artist": "MIT",
|
||||
"year": "1961",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": ""
|
||||
}
|
||||
}
|
||||
|
||||
00:00:50.901 --> 00:00:52.200
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT: Progressions",
|
||||
"artist": "David and Sheri Espar",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "Kenneth Friedman (YouTube)",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:52.201 --> 00:00:53.999
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT AI Lab Films",
|
||||
"artist": "MIT",
|
||||
"year": "1950s",
|
||||
"notes": "Color-corrected",
|
||||
"retrieved_from": "MIT",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:54.000 --> 00:00:55.000
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT: Progressions",
|
||||
"artist": "David and Sheri Espar",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "Kenneth Friedman (YouTube)",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||
}
|
||||
}
|
||||
|
||||
00:00:55.100 --> 00:00:55.800
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "The Social Beaver",
|
||||
"artist": "Oscar H. Horovitz",
|
||||
"year": "1956",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=X4TriKZrszw"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
00:00:55.950 --> 00:01:01.600
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "MIT: Progressions",
|
||||
"artist": "David and Sheri Espar",
|
||||
"year": "1969",
|
||||
"notes": "AI-upscaled and frame-interpolated",
|
||||
"retrieved_from": "Kenneth Friedman (YouTube)",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=p3mq5E0GwLA&ab_channel=KennethFriedman"
|
||||
}
|
||||
}
|
||||
|
||||
00:01:01.700 --> 00:01:02.899
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "The Social Beaver",
|
||||
"artist": "Oscar H. Horovitz",
|
||||
"year": "1956",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=X4TriKZrszw"
|
||||
}
|
||||
}
|
||||
|
||||
00:01:02.900 --> 00:01:03.799
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Making Electrons Count",
|
||||
"artist": "Lloyd C. Sanford",
|
||||
"year": "c. 1950",
|
||||
"notes": "AI-upscaled and color-corrected",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=KH0tcv3nEQI"
|
||||
}
|
||||
}
|
||||
|
||||
00:01:03.800 --> 00:01:15.600
|
||||
{
|
||||
"uid": "3",
|
||||
"type": "video_source",
|
||||
"data": {
|
||||
"type": "video_source",
|
||||
"title": "Technology",
|
||||
"artist": "MIT",
|
||||
"year": "1934",
|
||||
"notes": "AI-upscaled and frame-interpolated. View Kathleen E. 23's <a href=\"https://mitadmissions.org/blogs/entry/5-historical-mit-videos/#video5\"><strong>post</strong></a>",
|
||||
"retrieved_from": "MIT Museum",
|
||||
"hyperlink": "https://www.youtube.com/watch?v=rsO_67xzymQ&ab_channel=FromtheVaultofMIT"
|
||||
}
|
||||
}
|
BIN
viewer/public/data/MIT Regressions intro video.mp4
Normal file
BIN
viewer/public/data/MIT Regressions intro video.mp4
Normal file
Binary file not shown.
8939
viewer/public/data/subs/mit regressions subtitles unfinished.vtt
Normal file
8939
viewer/public/data/subs/mit regressions subtitles unfinished.vtt
Normal file
File diff suppressed because it is too large
Load Diff
BIN
viewer/public/favicon.ico
Normal file
BIN
viewer/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
1
viewer/settings/.gitignore
vendored
1
viewer/settings/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
secret.py
|
|
@ -1,130 +0,0 @@
|
|||
"""
|
||||
Django settings for viewer project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.1.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
||||
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "viewer.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "viewer.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
STATIC_ROOT = BASE_DIR / 'serve/'
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
BASE_DIR / 'static/',
|
||||
)
|
||||
|
||||
MEDIA_ROOT = BASE_DIR / 'media/'
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
try:
|
||||
from .secret import *
|
||||
except ImportError:
|
||||
pass
|
|
@ -1,2 +0,0 @@
|
|||
SECRET_KEY=secret
|
||||
DEBUG=True
|
38
viewer/src/components/VideoPlayer.tsx
Normal file
38
viewer/src/components/VideoPlayer.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import ReactPlayer from "react-player/lazy";
|
||||
// import CustomReactPlayer from "./CustomReactPlayer";
|
||||
|
||||
interface PlayerRef {
|
||||
seeking: boolean;
|
||||
played: number;
|
||||
duration: number;
|
||||
seekTo: (time: number) => void;
|
||||
}
|
||||
// inherit prop playing from parent component
|
||||
export default function VideoPlayer({ playerRef, playing, videoUrl, transcriptUrl }: { playerRef: React.RefObject<PlayerRef>, playing: boolean, videoUrl: string, transcriptUrl: string }) {
|
||||
// ReactPlayer.addCustomPlayer(CustomReactPlayer);
|
||||
return (
|
||||
<ReactPlayer
|
||||
ref={playerRef}
|
||||
crossOrigin="anonymous"
|
||||
playing={playing}
|
||||
controls={true}
|
||||
// url="https://youtu.be/TGKk3iwoI9I"
|
||||
url={videoUrl}
|
||||
onSeek={e => console.log('onSeek', e)}
|
||||
config={{
|
||||
file: {
|
||||
forceVideo: true,
|
||||
tracks: [
|
||||
{
|
||||
label: 'English',
|
||||
kind: 'captions',
|
||||
src: transcriptUrl,
|
||||
srcLang: 'en',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
34
viewer/src/components/WebVttPlayer/Metadata.tsx
Normal file
34
viewer/src/components/WebVttPlayer/Metadata.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { Component } from 'react'
|
||||
import MetadataPoint from './MetadataPoint'
|
||||
|
||||
class Metadata extends Component<MetadataProps> {
|
||||
|
||||
render() {
|
||||
const lines = []
|
||||
if (this.props.track && this.props.track.cues) {
|
||||
for (let i = 0; i < this.props.track.cues.length; i++) {
|
||||
lines.push(
|
||||
<MetadataPoint
|
||||
key={`point-${i}`}
|
||||
cue={this.props.track.cues[i]}
|
||||
active={false}
|
||||
seek={this.props.seek} />
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="track">
|
||||
{lines}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MetadataProps = {
|
||||
url: string,
|
||||
track: TextTrack,
|
||||
seek: (time: number) => void
|
||||
}
|
||||
|
||||
export default Metadata
|
212
viewer/src/components/WebVttPlayer/MetadataPoint.tsx
Normal file
212
viewer/src/components/WebVttPlayer/MetadataPoint.tsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
import React, { Component } from 'react'
|
||||
// import './MetadataPoint.css'
|
||||
|
||||
|
||||
|
||||
type MetadataPointProps = {
|
||||
cue: VTTCue
|
||||
active: boolean
|
||||
seek: (time: number) => void
|
||||
}
|
||||
|
||||
interface MetadataPointData {
|
||||
"uid": string,
|
||||
"type": string,
|
||||
// data is one of four interfaces: music, commentary, transcript, or video_source
|
||||
"data": MusicData | CommentaryData | TranscriptData | VideoSourceData | NarrationSourceData | OhmsData
|
||||
}
|
||||
|
||||
interface MusicData {
|
||||
"type": "music",
|
||||
"title": string,
|
||||
"title_alt": string,
|
||||
"artist": string,
|
||||
"year": string,
|
||||
"label": string,
|
||||
"hyperlink": string,
|
||||
}
|
||||
|
||||
interface CommentaryData {
|
||||
"type": "commentary",
|
||||
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
|
||||
}
|
||||
|
||||
interface TranscriptData {
|
||||
"type": "transcript",
|
||||
"text": string, // may contain VTT-compatible styling, as specified at https://www.w3.org/TR/webvtt1/#model-overview
|
||||
}
|
||||
|
||||
interface VideoSourceData {
|
||||
"type": "video_source",
|
||||
"title": string,
|
||||
"artist": string, // optional - may be empty string
|
||||
"attribution": string,
|
||||
"year": string,
|
||||
"notes": string,
|
||||
"retrieved_from": string,
|
||||
"hyperlink": string,
|
||||
}
|
||||
|
||||
interface NarrationSourceData {
|
||||
"type": "narration_source",
|
||||
"title": string,
|
||||
"attribution": string,
|
||||
"year": string,
|
||||
"retrieval_date": string,
|
||||
"source_type" : string, // PDF, etc
|
||||
|
||||
"hyperlink": string,
|
||||
}
|
||||
|
||||
// adopting the OHMS standard http://ohda.matrix.msu.edu/2014/11/indexing-interviews-in-ohms/
|
||||
interface OhmsData {
|
||||
"type": "ohms",
|
||||
"title": string,
|
||||
"title_alt": string,
|
||||
"synopsis": string,
|
||||
"synopsis_alt": string,
|
||||
"keywords": string,
|
||||
"keywords_alt": string,
|
||||
"subjects": string,
|
||||
"subjects_alt": string,
|
||||
"gpspoints": {
|
||||
"gps": string,
|
||||
"gps_zoom": string,
|
||||
"gps_text": string,
|
||||
"gps_text_alt": string
|
||||
},
|
||||
"hyperlinks": {
|
||||
"hyperlink": string,
|
||||
"hyperlink_text": string,
|
||||
"hyperlink_text_alt": string
|
||||
}
|
||||
}
|
||||
class MetadataPoint extends Component<MetadataPointProps, { isActive: boolean}> {
|
||||
|
||||
|
||||
constructor(props: MetadataPointProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isActive: false
|
||||
}
|
||||
this.props.cue.onenter = this.onEnter.bind(this)
|
||||
this.props.cue.onexit = this.onExit.bind(this)
|
||||
this.onClick = this.onClick.bind(this)
|
||||
// get current theme (but in a class component)
|
||||
}
|
||||
|
||||
render() {
|
||||
let style = ''
|
||||
if (this.state.isActive) {
|
||||
// active
|
||||
style = "bg-gray-200"
|
||||
}
|
||||
// exect JSON.parse data to be of type MetadataPointData
|
||||
const point = JSON.parse(this.props.cue.text) as MetadataPointData
|
||||
const data = point.data
|
||||
|
||||
let song = null
|
||||
let footage = null
|
||||
|
||||
// get type of data
|
||||
if (point.type == "music") {
|
||||
// const song is point.data as MusicData
|
||||
song = data as MusicData
|
||||
}
|
||||
|
||||
if (point.type == "video_source") {
|
||||
footage = data as VideoSourceData
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`point ${style}`}>
|
||||
<div className="time" onClick={this.onClick}>
|
||||
[{this.startTime()} - {this.endTime()}]
|
||||
</div>
|
||||
<div className="text">
|
||||
{point.type == "music" &&
|
||||
<div className="music" onClick={this.onClick}>
|
||||
<SongCard song={song} />
|
||||
</div>
|
||||
}
|
||||
{point.type == "video_source" &&
|
||||
<div className="footage" onClick={this.onClick}>
|
||||
<VideoSourceCard videoSource={footage} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.props.seek(this.props.cue.startTime)
|
||||
}
|
||||
|
||||
startTime() {
|
||||
return this.formatSeconds(this.props.cue.startTime)
|
||||
}
|
||||
|
||||
endTime() {
|
||||
return this.formatSeconds(this.props.cue.endTime)
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
this.setState({isActive: true})
|
||||
}
|
||||
|
||||
onExit() {
|
||||
this.setState({isActive: false})
|
||||
}
|
||||
|
||||
formatSeconds(t) {
|
||||
let mins = Math.floor(t / 60)
|
||||
if (mins < 10) {
|
||||
mins = `0${mins}`
|
||||
}
|
||||
|
||||
let secs = Math.floor(t % 60)
|
||||
if (secs < 10) {
|
||||
secs = `0${secs}`
|
||||
}
|
||||
|
||||
return `${mins}:${secs}`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const VideoSourceCard: React.FC<VideoSourceData> = ({videoSource}) => {
|
||||
return (
|
||||
<div className="bg-white shadow-md p-4">
|
||||
<h2 className="text-lg font-bold">{videoSource.title}</h2>
|
||||
<div className="text-gray-600">
|
||||
<p className="mb-1">{videoSource.artist}</p>
|
||||
<p className="mb-1">{videoSource.year}</p>
|
||||
{/* render the html that may be inside of {videoSource.notes} */}
|
||||
<div className="mb-1" dangerouslySetInnerHTML={{__html: videoSource.notes}} />
|
||||
</div>
|
||||
<a
|
||||
href={videoSource.hyperlink}
|
||||
className="top-0 right-0 bottom-0 bg-gray-500 hover:bg-gray-400 text-white text-xs font-bold px-3 py-1"
|
||||
>
|
||||
Link
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: fix typing
|
||||
const SongCard: React.FC<MusicData> = ({song}) => {
|
||||
return (
|
||||
<div className="bg-yellow-50 shadow-md p-4">
|
||||
<h2 className="text-lg font-bold">{song.title}</h2>
|
||||
<div className="text-gray-600">
|
||||
<p className="mb-1">{song.artist}</p>
|
||||
<p className="mb-1">{song.year}</p>
|
||||
<p className="mb-1">{song.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataPoint
|
25
viewer/src/components/WebVttPlayer/Search.module.css
Normal file
25
viewer/src/components/WebVttPlayer/Search.module.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
.search {
|
||||
border-top: thin solid #ccc;
|
||||
padding: 5px 5px 5px 5px;
|
||||
font-size: 14pt;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.search .container {
|
||||
border: thin solid #ccc;
|
||||
}
|
||||
|
||||
.search input {
|
||||
font-size: 12pt;
|
||||
border: none;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.search input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search .icon {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
24
viewer/src/components/WebVttPlayer/Search.tsx
Normal file
24
viewer/src/components/WebVttPlayer/Search.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { FunctionComponent } from "react" // TODO: fix warning
|
||||
import styles from "./Search.module.css"
|
||||
|
||||
const Search: FunctionComponent<SearchProps> = ({ query, updateQuery }) => {
|
||||
return (
|
||||
<div className={styles.search}>
|
||||
<div className={styles.container}>
|
||||
<span className={styles.icon}>🔍</span>
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => updateQuery(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// set prop types using TypeScript
|
||||
type SearchProps = {
|
||||
query: string,
|
||||
updateQuery: (query: string) => void
|
||||
}
|
||||
|
||||
|
||||
export default Search
|
37
viewer/src/components/WebVttPlayer/Transcript.tsx
Normal file
37
viewer/src/components/WebVttPlayer/Transcript.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React, { Component } from 'react'
|
||||
import TranscriptLine from './TranscriptLine'
|
||||
// import './Track.css' // currently exists in global instead. TODO: consolidate this styling
|
||||
|
||||
class Transcript extends Component<TranscriptProps> {
|
||||
|
||||
render() {
|
||||
const lines = []
|
||||
if (this.props.track && this.props.track.cues) {
|
||||
for (let i = 0; i < this.props.track.cues.length; i++) {
|
||||
lines.push(
|
||||
<TranscriptLine
|
||||
key={`line-${i}`}
|
||||
cue={this.props.track.cues[i]}
|
||||
active={false}
|
||||
seek={this.props.seek}
|
||||
query={this.props.query} />
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="track">
|
||||
{lines}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type TranscriptProps = {
|
||||
track: TextTrack,
|
||||
url: string,
|
||||
seek: (time: number) => void,
|
||||
query: string
|
||||
}
|
||||
|
||||
export default Transcript
|
79
viewer/src/components/WebVttPlayer/TranscriptLine.tsx
Normal file
79
viewer/src/components/WebVttPlayer/TranscriptLine.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { Component } from 'react'
|
||||
import './TranscriptLine.module.css'
|
||||
|
||||
class TranscriptLine extends Component<TranscriptLineProps, { isActive: boolean }> {
|
||||
|
||||
constructor(props: TranscriptLineProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isActive: false
|
||||
}
|
||||
this.props.cue.onenter = this.onEnter.bind(this)
|
||||
this.props.cue.onexit = this.onExit.bind(this)
|
||||
this.onClick = this.onClick.bind(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
let style = ''
|
||||
if (this.props.query && this.props.cue.text.match(new RegExp(this.props.query, 'i'))) {
|
||||
style = 'match'
|
||||
} else if (this.state.isActive) {
|
||||
style = 'active'
|
||||
}
|
||||
|
||||
// note: dangerouslySetInnerHTML is used because the text may contain HTML
|
||||
return (
|
||||
<div className={`${style} line`} onClick={this.onClick}>
|
||||
<div className="time">
|
||||
[{this.startTime()} - {this.endTime()}]
|
||||
</div>
|
||||
<div
|
||||
className={`${style} text`}
|
||||
dangerouslySetInnerHTML={{__html: this.props.cue.text}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
this.setState({isActive: true})
|
||||
}
|
||||
|
||||
onExit() {
|
||||
this.setState({isActive: false})
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.props.seek(this.props.cue.startTime)
|
||||
}
|
||||
|
||||
startTime() {
|
||||
return this.formatSeconds(this.props.cue.startTime)
|
||||
}
|
||||
|
||||
endTime() {
|
||||
return this.formatSeconds(this.props.cue.endTime)
|
||||
}
|
||||
|
||||
formatSeconds(t) {
|
||||
let mins = Math.floor(t / 60)
|
||||
if (mins < 10) {
|
||||
mins = `0${mins}`
|
||||
}
|
||||
|
||||
let secs = Math.floor(t % 60)
|
||||
if (secs < 10) {
|
||||
secs = `0${secs}`
|
||||
}
|
||||
|
||||
return `${mins}:${secs}`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type TranscriptLineProps = {
|
||||
cue: TextTrackCue,
|
||||
seek: (time: number) => void,
|
||||
query: string
|
||||
}
|
||||
|
||||
export default TranscriptLine
|
168
viewer/src/components/WebVttPlayer/WebVttPlayer.tsx
Normal file
168
viewer/src/components/WebVttPlayer/WebVttPlayer.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import { useLayoutEffect, useState, useEffect, useRef } from 'react';
|
||||
import dynamic from "next/dynamic";
|
||||
// const ReactPlayer = dynamic(() => import("react-player/lazy"), { ssr: false });
|
||||
import Transcript from './Transcript'
|
||||
import Metadata from './Metadata'
|
||||
import Search from './Search'
|
||||
const VideoPlayer = dynamic(() => import("../VideoPlayer"), { ssr: false });
|
||||
|
||||
type WebVttPlayerProps = {
|
||||
audio: string,
|
||||
videoUrl: string, // TODO: make naming scheme consistent lol
|
||||
transcript: string,
|
||||
metadataUrl: string,
|
||||
preload: boolean,
|
||||
};
|
||||
|
||||
interface ReactPlayerRef {
|
||||
seeking: boolean;
|
||||
played: number;
|
||||
duration: number;
|
||||
seekTo: (time: number) => void;
|
||||
}
|
||||
|
||||
interface NativePlayerRef {
|
||||
currentTime: number;
|
||||
ended: boolean;
|
||||
loop: boolean;
|
||||
muted: boolean;
|
||||
play: () => void;
|
||||
}
|
||||
|
||||
//Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format
|
||||
// const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export default function WebVttPlayer(props: WebVttPlayerProps) {
|
||||
const [trackLoaded, setTrackLoaded] = useState(false);
|
||||
const [metatrackLoaded, setMetatrackLoaded] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
// TODO: determine if these should be set
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const trackRef = useRef<TrackEvent>(null);
|
||||
const metatrackRef = useRef<TrackEvent>(null);
|
||||
|
||||
const reactPlayerRef = useRef<ReactPlayerRef>(null);
|
||||
const nativePlayerRef = useRef<NativePlayerRef>(null);
|
||||
|
||||
const preload = props.preload ? "true" : "false"
|
||||
|
||||
//There are 3 possible states: (1) loading when data is null (2) ready when the data is returned (3) error when there was an error fetching the data
|
||||
// const { data, error } = useSWR('/api/staticdata', fetcher);
|
||||
|
||||
//Handle the error state
|
||||
// if (error) console.log("Failed to load from SWR!"); //return <div>Failed to load</div>;
|
||||
//Handle the loading state
|
||||
// if (!data) console.log("Loading with SWR...")// return <div>Loading...</div>;
|
||||
|
||||
useEffect(() => {
|
||||
// Get a reference to the track and metatrack elements
|
||||
// TODO: this manual timeout is extremely gross! figure out how to conditionally rendder <Transcript> and <Metadata> according to loading of these refs without this awful hard-coded thing. NextJs certainly supports something better for on-time ref loading (SWR? getProps?)
|
||||
function checkIfLoaded(tries = 0) {
|
||||
tries += 1
|
||||
const track = trackRef.current;
|
||||
const metatrack = metatrackRef.current;
|
||||
if (track && track.track && track.track.cues && track.track.cues.length > 0) {
|
||||
setTrackLoaded(true);
|
||||
}
|
||||
if (metatrack && metatrack.track && metatrack.track.cues && metatrack.track.cues.length > 0) {
|
||||
setMetatrackLoaded(true);
|
||||
}
|
||||
else if (!metatrackLoaded || !trackLoaded) {
|
||||
const wait = 25 * Math.pow(tries, 2)
|
||||
setTimeout(() => checkIfLoaded(tries), wait);
|
||||
}
|
||||
}
|
||||
checkIfLoaded();
|
||||
|
||||
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
// if we figure out how to get access to Track refs with react-player (even in YouTube videos! That would be awesome), then we can start using this
|
||||
function reactPlayerSeek(secs: string) {
|
||||
|
||||
if (reactPlayerRef.current) {
|
||||
reactPlayerRef.current.seekTo(parseFloat(secs))
|
||||
}
|
||||
setPlaying(true);
|
||||
}
|
||||
|
||||
function seek(secs: number) {
|
||||
if (nativePlayerRef.current) {
|
||||
|
||||
nativePlayerRef.current.currentTime = secs;
|
||||
nativePlayerRef.current.play();
|
||||
}
|
||||
setPlaying(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full overflow-hidden">
|
||||
<div className="w-1/2 player">
|
||||
{/* <VideoPlayer playerRef={video} playing={playing} videoUrl={props.videoUrl} transcriptUrl={props.transcript} /> */}
|
||||
{/* a vanilla video element with source and tracks. so much easier oh my god */}
|
||||
<video
|
||||
// width="75%"
|
||||
preload={preload}
|
||||
// ignore ref errors for now
|
||||
ref={nativePlayerRef}
|
||||
crossOrigin="anonymous"
|
||||
controls={true}
|
||||
autoPlay={true}
|
||||
>
|
||||
<source src={props.videoUrl} type="video/mp4" />
|
||||
<track
|
||||
ref={trackRef}
|
||||
kind="subtitles"
|
||||
src={props.transcript}
|
||||
srcLang="en"
|
||||
default={true}
|
||||
/>
|
||||
<track default
|
||||
kind="metadata"
|
||||
src={props.metadataUrl}
|
||||
ref={metatrackRef} />
|
||||
</video>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<div className="webvtt-player">
|
||||
<div className="media h-1\/3">
|
||||
<div className="tracks">
|
||||
{trackLoaded ? (
|
||||
<>
|
||||
<Transcript
|
||||
url={props.transcript}
|
||||
seek={seek}
|
||||
track={trackRef.current.track}
|
||||
query={query}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"Loading transcript..."
|
||||
)}
|
||||
{metatrackLoaded && props.metadataUrl ? (
|
||||
<>
|
||||
<Metadata
|
||||
url={props.metadataUrl}
|
||||
seek={seek}
|
||||
track={metatrackRef.current.track}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
"\nLoading metadata..." // TODO: make better logic for showing if we wanna serve metadata
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
23
viewer/src/pages/_app.tsx
Normal file
23
viewer/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { type AppType } from "next/app";
|
||||
import { type Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
import {ThemeProvider} from 'next-themes'
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<ThemeProvider attribute="class">
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default trpc.withTRPC(MyApp);
|
30
viewer/src/pages/api/auth/[...nextauth].ts
Normal file
30
viewer/src/pages/api/auth/[...nextauth].ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import NextAuth, { type NextAuthOptions } from "next-auth";
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
// Prisma adapter for NextAuth, optional and can be removed
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
||||
|
||||
import { env } from "../../../env/server.mjs";
|
||||
import { prisma } from "../../../server/db/client";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// Include user.id on session
|
||||
callbacks: {
|
||||
session({ session, user }) {
|
||||
if (session.user) {
|
||||
session.user.id = user.id;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
// Configure one or more authentication providers
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
DiscordProvider({
|
||||
clientId: env.DISCORD_CLIENT_ID,
|
||||
clientSecret: env.DISCORD_CLIENT_SECRET,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
};
|
||||
|
||||
export default NextAuth(authOptions);
|
10
viewer/src/pages/api/examples.ts
Normal file
10
viewer/src/pages/api/examples.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
import { prisma } from "../../server/db/client";
|
||||
|
||||
const examples = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const examples = await prisma.example.findMany();
|
||||
res.status(200).json(examples);
|
||||
};
|
||||
|
||||
export default examples;
|
21
viewer/src/pages/api/restricted.ts
Normal file
21
viewer/src/pages/api/restricted.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
import { getServerAuthSession } from "../../server/common/get-server-auth-session";
|
||||
|
||||
const restricted = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
if (session) {
|
||||
res.send({
|
||||
content:
|
||||
"This is protected content. You can access this content because you are signed in.",
|
||||
});
|
||||
} else {
|
||||
res.send({
|
||||
error:
|
||||
"You must be signed in to view the protected content on this page.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default restricted;
|
17
viewer/src/pages/api/trpc/[trpc].ts
Normal file
17
viewer/src/pages/api/trpc/[trpc].ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
|
||||
import { env } from "../../../env/server.mjs";
|
||||
import { createContext } from "../../../server/trpc/context";
|
||||
import { appRouter } from "../../../server/trpc/router/_app";
|
||||
|
||||
// export API handler
|
||||
export default createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
onError:
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(`❌ tRPC failed on ${path}: ${error}`);
|
||||
}
|
||||
: undefined,
|
||||
});
|
88
viewer/src/pages/index.tsx
Normal file
88
viewer/src/pages/index.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import React, { useRef } from "react";
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import WebVttPlayer from "../components/WebVttPlayer/WebVttPlayer";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
|
||||
const hello = trpc.example.hello.useQuery({ text: "from tRPC" });
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const videoUrl = '/data/MIT Regressions intro video.mp4' // BIZARRE BUG: react-player component only works when I live-reload URL to valid path. if Chrome loads directly, then returns "failed to load media".
|
||||
const audioUrl = router.asPath + 'data/MIT Regressions intro audio.mp3'
|
||||
const transcriptUrl = router.asPath + "data/MIT Regressions intro captions.vtt"
|
||||
const metadataUrl = router.asPath + "data/MIT Regressions intro metadata.vtt"
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>viewer</title>
|
||||
<meta name="description" content="Generated by create-t3-app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-white dark:bg-black">
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 py-2 px-4 absolute top-4 right-4">
|
||||
toggle theme
|
||||
</button>
|
||||
<div className="px-4 py-32 pb-0 position-absolute">
|
||||
<div className="titles">
|
||||
<h1 className="text-1xl font-extrabold tracking-tight text-black dark:text-white sm:text-[3rem] top-4 left-4 absolute">
|
||||
documentary metadata viewer
|
||||
</h1>
|
||||
{/* hug the left-hand side of the screen */}
|
||||
<h2 className=" tracking-tight text-gray-500 dark:text-white top-20 left-6 absolute"><i>current film</i>: <a href="https://www.youtube.com/watch?v=TGKk3iwoI9I&ab_channel=MIT%3AREGRESSIONS">MIT: REGRESSIONS intro</a> | <a href="https://github.com/mit-regressions/viewer/blob/igel-t3-initial/viewer/public/data/MIT%20Regressions%20intro%20metadata.vtt">metadata source</a></h2>
|
||||
{/* <h2 className=" tracking-tight text-gray-500 dark:text-white top-24 left-6 absolute"><i>metadata file source (VTT)</i>: <a href="https://www.youtube.com/watch?v=TGKk3iwoI9I&ab_channel=MIT%3AREGRESSIONS">MIT: REGRESSIONS intro</a></h2> */}
|
||||
</div>
|
||||
|
||||
<WebVttPlayer
|
||||
preload={true}
|
||||
audio={audioUrl}
|
||||
videoUrl={videoUrl}
|
||||
transcript={transcriptUrl}
|
||||
metadataUrl={metadataUrl}
|
||||
/>
|
||||
|
||||
{/* <div className="flex flex-col items-center gap-2">
|
||||
<AuthShowcase />
|
||||
</div> */}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
||||
const AuthShowcase: React.FC = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
|
||||
const { data: secretMessage } = trpc.auth.getSecretMessage.useQuery(
|
||||
undefined, // no input
|
||||
{ enabled: sessionData?.user !== undefined },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-center text-2xl text-white">
|
||||
{sessionData && <span>Logged in as {sessionData.user?.name}</span>}
|
||||
{secretMessage && <span> - {secretMessage}</span>}
|
||||
</p>
|
||||
<button
|
||||
className="bg-white/10 px-10 py-3 font-semibold text-white no-underline transition hover:bg-white/20"
|
||||
onClick={sessionData ? () => signOut() : () => signIn()}
|
||||
>
|
||||
{sessionData ? "Sign out" : "Sign in"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
15
viewer/src/server/common/get-server-auth-session.ts
Normal file
15
viewer/src/server/common/get-server-auth-session.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { type GetServerSidePropsContext } from "next";
|
||||
import { unstable_getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "../../pages/api/auth/[...nextauth]";
|
||||
|
||||
/**
|
||||
* Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
|
||||
* See example usage in trpc createContext or the restricted API route
|
||||
*/
|
||||
export const getServerAuthSession = async (ctx: {
|
||||
req: GetServerSidePropsContext["req"];
|
||||
res: GetServerSidePropsContext["res"];
|
||||
}) => {
|
||||
return await unstable_getServerSession(ctx.req, ctx.res, authOptions);
|
||||
};
|
19
viewer/src/server/db/client.ts
Normal file
19
viewer/src/server/db/client.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "../../env/server.mjs";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
export const prisma =
|
||||
global.prisma ||
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== "production") {
|
||||
global.prisma = prisma;
|
||||
}
|
39
viewer/src/server/trpc/context.ts
Normal file
39
viewer/src/server/trpc/context.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { type inferAsyncReturnType } from "@trpc/server";
|
||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import { type Session } from "next-auth";
|
||||
|
||||
import { getServerAuthSession } from "../common/get-server-auth-session";
|
||||
import { prisma } from "../db/client";
|
||||
|
||||
type CreateContextOptions = {
|
||||
session: Session | null;
|
||||
};
|
||||
|
||||
/** Use this helper for:
|
||||
* - testing, so we dont have to mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||
**/
|
||||
export const createContextInner = async (opts: CreateContextOptions) => {
|
||||
return {
|
||||
session: opts.session,
|
||||
prisma,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you'll use in your router
|
||||
* @link https://trpc.io/docs/context
|
||||
**/
|
||||
export const createContext = async (opts: CreateNextContextOptions) => {
|
||||
const { req, res } = opts;
|
||||
|
||||
// Get the session from the server using the unstable_getServerSession wrapper function
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
return await createContextInner({
|
||||
session,
|
||||
});
|
||||
};
|
||||
|
||||
export type Context = inferAsyncReturnType<typeof createContext>;
|
11
viewer/src/server/trpc/router/_app.ts
Normal file
11
viewer/src/server/trpc/router/_app.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { router } from "../trpc";
|
||||
import { authRouter } from "./auth";
|
||||
import { exampleRouter } from "./example";
|
||||
|
||||
export const appRouter = router({
|
||||
example: exampleRouter,
|
||||
auth: authRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter;
|
10
viewer/src/server/trpc/router/auth.ts
Normal file
10
viewer/src/server/trpc/router/auth.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { router, publicProcedure, protectedProcedure } from "../trpc";
|
||||
|
||||
export const authRouter = router({
|
||||
getSession: publicProcedure.query(({ ctx }) => {
|
||||
return ctx.session;
|
||||
}),
|
||||
getSecretMessage: protectedProcedure.query(() => {
|
||||
return "you can now see this secret message!";
|
||||
}),
|
||||
});
|
16
viewer/src/server/trpc/router/example.ts
Normal file
16
viewer/src/server/trpc/router/example.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { router, publicProcedure } from "../trpc";
|
||||
|
||||
export const exampleRouter = router({
|
||||
hello: publicProcedure
|
||||
.input(z.object({ text: z.string().nullish() }).nullish())
|
||||
.query(({ input }) => {
|
||||
return {
|
||||
greeting: `Hello ${input?.text ?? "world"}`,
|
||||
};
|
||||
}),
|
||||
getAll: publicProcedure.query(({ ctx }) => {
|
||||
return ctx.prisma.example.findMany();
|
||||
}),
|
||||
});
|
39
viewer/src/server/trpc/trpc.ts
Normal file
39
viewer/src/server/trpc/trpc.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { type Context } from "./context";
|
||||
|
||||
const t = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape }) {
|
||||
return shape;
|
||||
},
|
||||
});
|
||||
|
||||
export const router = t.router;
|
||||
|
||||
/**
|
||||
* Unprotected procedure
|
||||
**/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/**
|
||||
* Reusable middleware to ensure
|
||||
* users are logged in
|
||||
*/
|
||||
const isAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Protected procedure
|
||||
**/
|
||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
165
viewer/src/styles/globals.css
Normal file
165
viewer/src/styles/globals.css
Normal file
|
@ -0,0 +1,165 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer components {
|
||||
|
||||
.webvtt-player {
|
||||
/* hug the left-hand side of the screen */
|
||||
/* @apply text-purple-500; */
|
||||
}
|
||||
|
||||
.track {
|
||||
/* flex: 6;
|
||||
height: 90vh;
|
||||
padding: 15px;
|
||||
overflow-y: scroll; */
|
||||
|
||||
/* implement the above styling with tailwind */
|
||||
|
||||
/* tailwind flex-CUSTOMNUMBER */
|
||||
/* @apply h-96 p-[15px] overflow-y-scroll flex-[6]; */
|
||||
/* @apply text-orange-200; /* this works lol */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Player */
|
||||
.webvtt-player .media {
|
||||
width: 100%;
|
||||
border: thin solid #ccc;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.webvtt-player audio {
|
||||
width: 100%;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.webvtt-player audio::-webkit-media-controls-panel {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.webvtt-player .tracks {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.webvtt-player .controls {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
vertical-align: text-top;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.webvtt-player .seekBar {
|
||||
vertical-align: middle;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
|
||||
.webvtt-player .track {
|
||||
flex: 6;
|
||||
height: 90vh;
|
||||
padding: 10px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* TranscriptLine */
|
||||
|
||||
.webvtt-player .active {
|
||||
/* background-color: #eee; */
|
||||
/* light border */
|
||||
@apply bg-gray-200 border-gray-600 border-2 border-opacity-10 border-x-0;
|
||||
}
|
||||
|
||||
.webvtt-player .match {
|
||||
/* background-color: lightyellow; */
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
.webvtt-player .line {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.webvtt-player .time {
|
||||
width: 110px;
|
||||
float: left;
|
||||
font-size: 10pt;
|
||||
line-height: 14pt;
|
||||
}
|
||||
|
||||
.webvtt-player .text {
|
||||
margin-left: 110px;
|
||||
}
|
||||
|
||||
|
||||
/* Metadatapoint */
|
||||
|
||||
.webvtt-player .point {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.webvtt-player .time {
|
||||
width: 110px;
|
||||
float: left;
|
||||
font-size: 10pt;
|
||||
line-height: 14pt;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.webvtt-player .text {
|
||||
margin-left: 110px;
|
||||
}
|
||||
|
||||
.webvtt-player .title {
|
||||
font-weight: bold;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.webvtt-player .titleAlt {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.webvtt-player .field {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.webvtt-player .field span {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
/* Search */
|
||||
.search {
|
||||
border-top: thin solid #ccc;
|
||||
padding: 5px 5px 5px 5px;
|
||||
font-size: 14pt;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.search .container {
|
||||
border: thin solid #ccc;
|
||||
}
|
||||
|
||||
.search input {
|
||||
font-size: 12pt;
|
||||
border: none;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.search input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search .icon {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
12
viewer/src/types/next-auth.d.ts
vendored
Normal file
12
viewer/src/types/next-auth.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { type DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
user?: {
|
||||
id: string;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
}
|
42
viewer/src/utils/trpc.ts
Normal file
42
viewer/src/utils/trpc.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { httpBatchLink, loggerLink } from "@trpc/client";
|
||||
import { createTRPCNext } from "@trpc/next";
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { type AppRouter } from "../server/trpc/router/_app";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||
};
|
||||
|
||||
export const trpc = createTRPCNext<AppRouter>({
|
||||
config() {
|
||||
return {
|
||||
transformer: superjson,
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === "development" ||
|
||||
(opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Inference helper for inputs
|
||||
* @example type HelloInput = RouterInputs['example']['hello']
|
||||
**/
|
||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
/**
|
||||
* Inference helper for outputs
|
||||
* @example type HelloOutput = RouterOutputs['example']['hello']
|
||||
**/
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
@ -1,55 +0,0 @@
|
|||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #FCEFC2
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background)
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Float four columns side by side */
|
||||
.column {
|
||||
float: left;
|
||||
width: 25%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* Remove extra left and right margins, due to padding */
|
||||
.row {margin: 0 -5px;}
|
||||
|
||||
/* Clear floats after the columns */
|
||||
.row:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* Responsive columns */
|
||||
@media screen and (max-width: 600px) {
|
||||
.column {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Style the counter cards */
|
||||
.card {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
background-color: #f1f1f1;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
12
viewer/tailwind.config.cjs
Normal file
12
viewer/tailwind.config.cjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['HelveticaNeue', 'Helvetica'],
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
{% load static %}
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="author" content="Rushil Umaretiya" />
|
||||
<title>MIT Regressions</title>
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
</html>
|
|
@ -1,38 +0,0 @@
|
|||
{% extends "player/base.html" %}
|
||||
{% block head %}
|
||||
<link href="https://vjs.zencdn.net/7.19.2/video-js.css" rel="stylesheet" />
|
||||
<style>
|
||||
.vjs-blue-theme .vjs-play-progress {
|
||||
background: #0000ff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<video
|
||||
id="my-player"
|
||||
class="video-js"
|
||||
controls
|
||||
preload="auto"
|
||||
poster="https://vjs.zencdn.net/v/oceans.png"
|
||||
data-setup='{}'>
|
||||
<source src="https://vjs.zencdn.net/v/oceans.mp4" type="video/mp4"></source>
|
||||
<source src="https://vjs.zencdn.net/v/oceans.webm" type="video/webm"></source>
|
||||
<source src="https://vjs.zencdn.net/v/oceans.ogv" type="video/ogg"></source>
|
||||
<p class="vjs-no-js">
|
||||
To view this video please enable JavaScript, and consider upgrading to a
|
||||
web browser that
|
||||
<a href="https://videojs.com/html5-video-support/" target="_blank">
|
||||
supports HTML5 video
|
||||
</a>
|
||||
</p>
|
||||
</video>
|
||||
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="card">
|
||||
<p>timestamp</p>
|
||||
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,14 +0,0 @@
|
|||
{% extends "player/base.html" %}
|
||||
{% load static %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{% static 'css/registration.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form method="post">
|
||||
<h1>login</h1>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">login</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,14 +0,0 @@
|
|||
{% extends "player/base.html" %}
|
||||
{% load static %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{% static 'css/registration.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<form method="post">
|
||||
<h1>register</h1>
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">sign up</button>
|
||||
</form>
|
||||
{% endblock %}
|
21
viewer/tsconfig.json
Normal file
21
viewer/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs", "webvtt-plaer.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
"""viewer URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts", include("viewer.apps.users.urls")),
|
||||
path("", include("viewer.apps.player.urls")),
|
||||
]
|
1
viewer/webvtt-player.d.ts
vendored
Normal file
1
viewer/webvtt-player.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'webvtt-player';
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
WSGI config for viewer project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "viewer.settings")
|
||||
|
||||
application = get_wsgi_application()
|
Loading…
Reference in New Issue
Block a user