├── client ├── .eslintrc ├── public │ ├── locales │ │ ├── ko │ │ │ ├── video.json │ │ │ ├── main.json │ │ │ ├── pref.json │ │ │ ├── information.json │ │ │ └── filter.json │ │ └── en │ │ │ ├── video.json │ │ │ ├── main.json │ │ │ ├── pref.json │ │ │ ├── information.json │ │ │ └── filter.json │ ├── favicon.ico │ └── maintenance.html ├── next-i18next.config.js ├── contexts │ ├── LocaleContext.ts │ └── AutoplayContext.ts ├── next-env.d.ts ├── components │ ├── Maintenance.tsx │ ├── VideoCard.tsx │ ├── CustomNav.tsx │ └── FilterData.tsx ├── next.config.js ├── .gitignore ├── tsconfig.json ├── types │ └── types.ts ├── styles │ └── nprogress.css ├── package.json ├── README.md ├── store │ └── index.ts └── pages │ ├── pref.tsx │ ├── _app.tsx │ ├── information.tsx │ ├── index.tsx │ └── video │ └── [id].tsx ├── dogfood ├── logo-markup │ ├── .eslintrc.json │ ├── netlify.toml │ ├── public │ │ ├── favicon.ico │ │ ├── vercel.svg │ │ └── next.svg │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── video.ts │ │ ├── index.tsx │ │ └── video │ │ │ └── [videoId].tsx │ ├── next.config.js │ ├── .gitignore │ ├── components │ │ ├── VideoPlayer.tsx │ │ ├── TimelinePlayer.tsx │ │ └── Rectangle.tsx │ ├── tsconfig.json │ ├── db │ │ ├── seed.js │ │ ├── dbConnect.ts │ │ ├── models │ │ │ └── Video.ts │ │ └── actions │ │ │ └── Video.ts │ ├── package.json │ ├── utils │ │ └── consts.ts │ └── README.md └── video-tag │ ├── client │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ ├── App.test.tsx │ │ ├── index.css │ │ ├── reportWebVitals.ts │ │ ├── index.tsx │ │ ├── App.css │ │ ├── logo.svg │ │ └── App.tsx │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── tsconfig.json │ ├── package.json │ └── README.md │ ├── api │ ├── tsconfig.json │ ├── package.json │ └── index.ts │ └── .gitignore ├── .github └── FUNDING.yml ├── CONTRIBUTING.md ├── README.md ├── .gitignore └── LICENSE /client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /client/public/locales/ko/video.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtitleMsg": "자막이 제공됩니다: " 3 | } 4 | -------------------------------------------------------------------------------- /dogfood/logo-markup/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /dogfood/logo-markup/netlify.toml: -------------------------------------------------------------------------------- 1 | [[plugins]] 2 | package = "@netlify/plugin-nextjs" -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katsukixyz/izone-archive/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/locales/en/video.json: -------------------------------------------------------------------------------- 1 | { 2 | "subtitleMsg": "The following subtitles are available for this video: " 3 | } 4 | -------------------------------------------------------------------------------- /dogfood/logo-markup/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katsukixyz/izone-archive/HEAD/dogfood/logo-markup/public/favicon.ico -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katsukixyz/izone-archive/HEAD/dogfood/video-tag/client/public/favicon.ico -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katsukixyz/izone-archive/HEAD/dogfood/video-tag/client/public/logo192.png -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katsukixyz/izone-archive/HEAD/dogfood/video-tag/client/public/logo512.png -------------------------------------------------------------------------------- /client/next-i18next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | i18n: { 3 | defaultLocale: 'en', 4 | locales: ['en', 'ko'], 5 | defaultNS: 'main' 6 | }, 7 | }; -------------------------------------------------------------------------------- /client/public/locales/ko/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "VLIVE 아카이브", 3 | "filterBtn": "필터", 4 | "information": "정보", 5 | "donate": "후원", 6 | "preferences": "설정" 7 | } 8 | -------------------------------------------------------------------------------- /client/public/locales/en/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "VLIVE Archive", 3 | "filterBtn": "Filter", 4 | "information": "Information", 5 | "donate": "Donate", 6 | "preferences": "Preferences" 7 | } 8 | -------------------------------------------------------------------------------- /client/contexts/LocaleContext.ts: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | const LocaleContext = createContext({ 3 | locale: "en", 4 | changeLocale: () => {}, 5 | }); 6 | 7 | export default LocaleContext; 8 | -------------------------------------------------------------------------------- /client/contexts/AutoplayContext.ts: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | const AutoplayContext = createContext({ 3 | autoplay: true, 4 | toggleAutoplay: () => {}, 5 | }); 6 | 7 | export default AutoplayContext; 8 | -------------------------------------------------------------------------------- /client/public/locales/ko/pref.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "설정", 3 | "language": { 4 | "label": "언어" 5 | }, 6 | "autoplay": { 7 | "label": "자동 재생", 8 | "description": "페이지 로드 후 자동으로 영상을 재생해요" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/public/locales/en/pref.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "Preferences", 3 | "language": { 4 | "label": "Language" 5 | }, 6 | "autoplay": { 7 | "label": "Autoplay", 8 | "description": "Play videos automatically on page load" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /dogfood/logo-markup/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import { ChakraProvider } from '@chakra-ui/react' 3 | 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /client/public/maintenance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Maintenance 5 | 6 | 7 |
8 |

404: Page not found

9 |

The page requested is currently undergoing maintenance.

10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /client/components/Maintenance.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Maintenance = () => { 4 | return ( 5 |
6 |

404: Page not found

7 |

The page requested is currently undergoing maintenance.

8 |
9 | ); 10 | }; 11 | 12 | export default Maintenance; 13 | -------------------------------------------------------------------------------- /dogfood/logo-markup/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /dogfood/video-tag/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2020", "dom"], 4 | "module": "commonjs", 5 | "target": "es2020", 6 | 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "include": ["index.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /dogfood/video-tag/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /dogfood/logo-markup/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | if ( 3 | process.env.LD_LIBRARY_PATH == null || 4 | !process.env.LD_LIBRARY_PATH.includes( 5 | `${process.env.PWD}/node_modules/canvas/build/Release:` 6 | ) 7 | ) { 8 | process.env.LD_LIBRARY_PATH = `${ 9 | process.env.PWD 10 | }/node_modules/canvas/build/Release:${process.env.LD_LIBRARY_PATH || ""}`; 11 | } 12 | 13 | const nextConfig = { 14 | reactStrictMode: true, 15 | }; 16 | 17 | module.exports = nextConfig; 18 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | const { i18n } = require("./next-i18next.config"); 2 | module.exports = { 3 | i18n, 4 | async redirects() { 5 | return [ 6 | process.env.MAINTENANCE_MODE === "1" 7 | ? { 8 | source: "/((?!maintenance).*)", 9 | destination: "/maintenance.html", 10 | permanent: false, 11 | } 12 | : null, 13 | ].filter(Boolean); 14 | }, 15 | reactStrictMode: true, 16 | images: { 17 | domains: ["hls.izonev.live"], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /dogfood/video-tag/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "scripts": { 6 | "start": "nodemon index.ts" 7 | }, 8 | "license": "MIT", 9 | "dependencies": { 10 | "@types/cors": "^2.8.12", 11 | "@types/express": "^4.17.13", 12 | "body-parser": "^1.19.0", 13 | "cors": "^2.8.5", 14 | "express": "^4.17.1" 15 | }, 16 | "devDependencies": { 17 | "nodemon": "^2.0.12", 18 | "ts-node": "^10.1.0", 19 | "typescript": "^4.3.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/.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 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /dogfood/logo-markup/.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 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /dogfood/logo-markup/components/VideoPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { VIDEO_HEIGHT, VIDEO_WIDTH } from "@/utils/consts"; 2 | import React from "react"; 3 | import ReactPlayer from "react-player/lazy"; 4 | 5 | interface VideoPlayerProps { 6 | playerRef: any; 7 | url: string; 8 | onReady: () => void; 9 | } 10 | 11 | export default function VideoPlayer({ 12 | playerRef, 13 | url, 14 | onReady, 15 | }: VideoPlayerProps) { 16 | return ( 17 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /dogfood/logo-markup/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/locales/ko/information.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "정보", 3 | "faq": { 4 | "heading": "FAQ", 5 | "1": { 6 | "q": "VLIVE 서비스 종료시 이 사이트 이용이 중지되나요?", 7 | "a": "아니요, 이 사이트는 VLIVE 서비스와 관계없이 계속 운영돼요. 이 사이트는 VLIVE 서버와 독립적이에요." 8 | }, 9 | "2": { 10 | "q": "삭제된 VLIVE 영상도 제공되나요?", 11 | "a": "네, \"삭제됨\" 태그를 통해 찾을 수 있어요." 12 | } 13 | }, 14 | "issues": { 15 | "heading": "오류 신고 및 개선 제안", 16 | "description": "Github 레포스트리에 관련 Issue를 발행하시면 신속히 처리해드려요." 17 | }, 18 | "misc": { 19 | "heading": "기타", 20 | "description": "기타 질문 사항은 Github Issue나 katsukidotxyz@gmail.com로 연락해주세요." 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "downlevelIteration": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /client/types/types.ts: -------------------------------------------------------------------------------- 1 | import { tagCategories } from "../components/FilterData"; 2 | 3 | export type VideoMeta = { 4 | id: string; 5 | date: string; 6 | title: string; 7 | duration: number; 8 | video: string; 9 | thumbnail: string; 10 | subtitles: SubtitleMeta[]; 11 | tags: string[]; 12 | koTitle?: string; 13 | }; 14 | 15 | export type SubtitleMeta = { 16 | code: string; 17 | url: string; 18 | }; 19 | 20 | export type SortOption = { 21 | value: "desc" | "asc"; 22 | label: "Most to least recent" | "Least to most recent"; 23 | }; 24 | 25 | export type TagOption = { 26 | value: Tags; 27 | label: Tags; 28 | }; 29 | 30 | type Tags = typeof tagCategories[number]; 31 | -------------------------------------------------------------------------------- /dogfood/logo-markup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "db/seed.js"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /dogfood/logo-markup/db/seed.js: -------------------------------------------------------------------------------- 1 | import dbConnect from "./dbConnect"; 2 | import meta from "../meta/meta.json"; 3 | import Video from "./models/Video"; 4 | 5 | const init = async () => { 6 | await dbConnect(); 7 | 8 | console.info("Creating videos"); 9 | const videos = await Promise.all( 10 | meta.map(async (meta, i) => { 11 | const video = new Video({ 12 | id: meta.id, 13 | actions: [], 14 | completed: false, 15 | }); 16 | 17 | await video.save(); 18 | 19 | return video; 20 | }) 21 | ); 22 | 23 | console.info("Done"); 24 | process.exit(); 25 | }; 26 | 27 | try { 28 | init(); 29 | } catch (e) { 30 | console.error(e); 31 | } 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: katsukixyz 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /client/styles/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: var(--izone-color); 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px var(--izone-color), 0 0 5px var(--izone-color); 26 | opacity: 1; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dogfood/logo-markup/db/dbConnect.ts: -------------------------------------------------------------------------------- 1 | declare var global: any; 2 | 3 | import { db } from "@/utils/consts"; 4 | import mongoose from "mongoose"; 5 | 6 | mongoose.set("strictQuery", false); 7 | 8 | let cached = global.mongoose; 9 | 10 | if (!cached) { 11 | cached = global.mongoose = { conn: null, promise: null }; 12 | } 13 | 14 | async function dbConnect() { 15 | if (cached.conn) { 16 | return cached.conn; 17 | } 18 | 19 | if (!cached.promise) { 20 | cached.promise = mongoose 21 | .connect(db.dbUrl as string, { 22 | dbName: db.dbName, 23 | }) 24 | .then((mongoose) => { 25 | return mongoose; 26 | }); 27 | } 28 | cached.conn = await cached.promise; 29 | return cached.conn; 30 | } 31 | 32 | export default dbConnect; 33 | -------------------------------------------------------------------------------- /client/public/locales/ko/filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "videosFound": "찾은 영상: ", 3 | "startDate": "시작 날짜", 4 | "endDate": "종료 날짜", 5 | "desc": "정확도순", 6 | "asc": "정확도역순", 7 | "search": "제목", 8 | "tags": { 9 | "placeholder": "태그", 10 | "live": "라이브", 11 | "deleted": "삭제됨", 12 | "vpick": "VPICK", 13 | "enozi": "ENOZI", 14 | "promotion": "프로모션", 15 | "mv": "MV", 16 | "cheerGuide": "응원법", 17 | "makingFilm": "메이킹필름", 18 | "liev": "LieV", 19 | "starRoad": "스타로드", 20 | "idolRoom": "아이돌룸", 21 | "greetings": "인사 메시지", 22 | "dancePractice": "Dance Practice", 23 | "audioOnly": "Voice Only", 24 | "misc": "기타" 25 | }, 26 | "reset": "초기화", 27 | "dateDesc": "MM/DD/YYYY 형식으로 날짜를 입력해주세요", 28 | "tagDesc": "여러 태그를 선택하면 모든 태그에 대한 결과가 표시돼요" 29 | } 30 | -------------------------------------------------------------------------------- /dogfood/video-tag/api/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import fs from "fs"; 3 | import cors from "cors"; 4 | import bodyParser from "body-parser"; 5 | 6 | const app = express(); 7 | app.use(cors()); 8 | app.use(bodyParser.json({ limit: "50mb" })); 9 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); 10 | app.use(express.json()); 11 | 12 | app.get("/", (req: express.Request, res: express.Response) => { 13 | const videoMeta = JSON.parse(fs.readFileSync("meta.json", "utf-8")); 14 | res.status(200).json(videoMeta); 15 | }); 16 | 17 | app.post("/", (req: express.Request, res: express.Response) => { 18 | const videoMeta = JSON.stringify(req.body); 19 | fs.writeFileSync("meta.json", videoMeta); 20 | res.status(200).json({}); 21 | }); 22 | 23 | app.listen(5000, () => console.log("Listening.")); 24 | -------------------------------------------------------------------------------- /dogfood/logo-markup/db/models/Video.ts: -------------------------------------------------------------------------------- 1 | import { ActionRectangle } from "@/pages/video/[videoId]"; 2 | import mongoose from "mongoose"; 3 | const { Schema } = mongoose; 4 | 5 | export interface IVideo { 6 | id: string; 7 | actions: ActionRectangle[]; 8 | completed: boolean; 9 | } 10 | 11 | const videoSchema = new Schema({ 12 | id: { 13 | type: String, 14 | index: true, 15 | }, 16 | actions: { 17 | type: [ 18 | { 19 | id: { type: String }, 20 | start: { type: Number }, 21 | end: { type: Number }, 22 | effectId: { type: String }, 23 | x: { type: Number }, 24 | y: { type: Number }, 25 | width: { type: Number }, 26 | height: { type: Number }, 27 | maxEnd: { type: Number }, 28 | }, 29 | ], 30 | }, 31 | completed: Boolean, 32 | }); 33 | 34 | export default mongoose.models.Video || mongoose.model("Video", videoSchema); 35 | -------------------------------------------------------------------------------- /client/public/locales/en/information.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "Information", 3 | "faq": { 4 | "heading": "FAQ", 5 | "1": { 6 | "q": "Will this site go offline if VLIVE services are terminated?", 7 | "a": "No, this site will remain operational regardless of VLIVE status. The site runs independently of VLIVE's servers." 8 | }, 9 | "2": { 10 | "q": "Does the archive include deleted VLIVEs?", 11 | "a": "Yes, use the filter by tags option and select the \"Deleted\" tag." 12 | } 13 | }, 14 | "issues": { 15 | "heading": "Report issues/feature suggestions", 16 | "description": "Create a new issue on the GitHub repository with the relevant details and reproduction methods and it will be dealt with promptly." 17 | }, 18 | "misc": { 19 | "heading": "Miscellaneous", 20 | "description": "For any miscellaneous questions, either leave an issue on GitHub or email me at katsukidotxyz@gmail.com." 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/public/locales/en/filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "videosFound": "Videos found: ", 3 | "startDate": "Start date", 4 | "endDate": "End date", 5 | "desc": "Most to least recent", 6 | "asc": "Least to most recent", 7 | "search": "Search titles", 8 | "tags": { 9 | "placeholder": "Filter by tags", 10 | "live": "Live", 11 | "deleted": "Deleted", 12 | "vpick": "VPICK", 13 | "enozi": "ENOZI", 14 | "promotion": "Promotion", 15 | "mv": "MV", 16 | "cheerGuide": "Cheer Guide", 17 | "makingFilm": "Making Film", 18 | "liev": "LieV", 19 | "starRoad": "Star Road", 20 | "idolRoom": "Idol Room", 21 | "greetings": "Greetings", 22 | "dancePractice": "Dance Practice", 23 | "audioOnly": "Audio Only", 24 | "misc": "Misc" 25 | }, 26 | "reset": "Reset", 27 | "dateDesc": "Enter start and end dates in the format MM/DD/YYYY", 28 | "tagDesc": "Selecting multiple tags displays results with any of the selected tags" 29 | } 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for considering to contribute! You're free to leave any issues or bugs related to the site, submit contributions to make improvements or provide any bug fixes, or provide any suggestions or feature requests. 4 | 5 | ## Pull requests 6 | 7 | 1. Fork the repository. 8 | 2. Create a separate branch off from the `staging` branch. 9 | 3. Commit your changes and submit your pull request. 10 | 11 | > Before submitting your PR, please make sure that your relevant changes have been pushed to the correct branch. `prod` should not be modified. 12 | 13 | ## Commit messages 14 | 15 | This project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) specification for commit messages. Please try to adhere to this standard when formatting your commit messages. 16 | 17 | ## Feature requests 18 | 19 | For any feature requests, please leave an issue with a description of the feature you would like implemented. You are welcome to provide any images or visuals to help convey your ideas. 20 | -------------------------------------------------------------------------------- /dogfood/logo-markup/db/actions/Video.ts: -------------------------------------------------------------------------------- 1 | import { HydratedDocument, UpdateQuery } from "mongoose"; 2 | import Video, { IVideo } from "../models/Video"; 3 | import dbConnect from "../dbConnect"; 4 | 5 | async function findVideo( 6 | videoId: string 7 | ): Promise | null> { 8 | await dbConnect(); 9 | try { 10 | return await Video.findOne({ id: videoId }); 11 | } catch (e) { 12 | return null; 13 | } 14 | } 15 | 16 | async function updateVideo(videoId: string, update: UpdateQuery) { 17 | await dbConnect(); 18 | return await Video.findOneAndUpdate({ id: videoId }, update); 19 | } 20 | 21 | async function addVideo(inputData: IVideo) { 22 | await dbConnect(); 23 | try { 24 | return await Video.create([inputData]); 25 | } catch (e) { 26 | return null; 27 | } 28 | } 29 | 30 | async function findAllVideos(): Promise[]> { 31 | await dbConnect(); 32 | try { 33 | return await Video.find(); 34 | } catch (e) { 35 | return []; 36 | } 37 | } 38 | 39 | export { findVideo, findAllVideos, updateVideo, addVideo }; 40 | -------------------------------------------------------------------------------- /dogfood/logo-markup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logo-markup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "seed": "node -r esm ./db/seed.ts", 11 | "export": "next export" 12 | }, 13 | "dependencies": { 14 | "@chakra-ui/react": "^2.7.0", 15 | "@emotion/react": "^11.11.1", 16 | "@emotion/styled": "^11.11.0", 17 | "@types/node": "20.3.1", 18 | "@types/react": "18.2.12", 19 | "@types/react-dom": "18.2.5", 20 | "@xzdarcy/react-timeline-editor": "^0.1.9", 21 | "canvas": "^2.11.2", 22 | "eslint": "8.43.0", 23 | "eslint-config-next": "13.4.6", 24 | "esm": "^3.2.25", 25 | "framer-motion": "^10.12.16", 26 | "konva": "^9.2.0", 27 | "mongoose": "^7.3.0", 28 | "next": "^12.3.4", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-konva": "^18.2.9", 32 | "react-player": "^2.12.0", 33 | "redux": "^4.2.1", 34 | "typescript": "5.1.3", 35 | "video-react": "^0.16.0" 36 | }, 37 | "devDependencies": { 38 | "@netlify/plugin-nextjs": "^4.38.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^4.6.2", 13 | "@chakra-ui/icons": "^1.0.15", 14 | "@chakra-ui/react": "^1.6.6", 15 | "@emotion/react": "^11", 16 | "@emotion/styled": "^11", 17 | "@types/nprogress": "^0.2.0", 18 | "@types/react-select": "^4.0.17", 19 | "@types/recoil": "^0.0.9", 20 | "bootstrap": "^5.0.2", 21 | "dayjs": "^1.10.6", 22 | "framer-motion": "^4", 23 | "next": "11.0.2-canary.28", 24 | "next-i18next": "^10.1.0", 25 | "nprogress": "^0.2.0", 26 | "react": "17.0.2", 27 | "react-bootstrap": "^1.6.1", 28 | "react-dom": "17.0.2", 29 | "react-icons": "^4.2.0", 30 | "react-infinite-scroll-component": "^6.1.0", 31 | "react-player": "^2.9.0", 32 | "react-select": "^4.3.1", 33 | "recoil": "^0.4.0" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "17.0.14", 37 | "eslint": "7.31.0", 38 | "eslint-config-next": "11.0.1", 39 | "typescript": "4.3.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # IZ\*ONE VLIVE Archive 5 | 6 | Media streaming platform for IZ\*ONE VLIVE livestreams 7 | 8 | ## Structure 9 | 10 | The frontend code for the main site is located in `client` using Next.js. See `Local Development` for how to run locally. 11 | 12 | The `dogfood` directory includes development environments to sandbox and build out new functionality: 13 | 14 | - `dogfood/video-tag` includes a frontend `create-react-app` application and a backend `Express` api to manually modify video metadata like adding/removing video types (VPICK, ENOZI, etc.) via a control panel. 15 | 16 | ## Contributions 17 | 18 | This project is open to contributions! Please see [CONTRIBUTING.md](https://github.com/katsukixyz/izone-archive/blob/prod/CONTRIBUTING.md) for guidelines. 19 | 20 | ## Local development 21 | 22 | Navigate to `client` and run the following: 23 | 24 | ``` 25 | yarn 26 | yarn dev 27 | ``` 28 | 29 | The frontend site can now be accessed from `http://localhost:3000`. 30 | 31 | ## Acknowledgements 32 | 33 | Big thanks to the `r/izone` subreddit for initial feedback and suggestions. 34 | -------------------------------------------------------------------------------- /dogfood/logo-markup/utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const VIDEO_WIDTH = 768; 2 | export const VIDEO_HEIGHT = 432; 3 | 4 | export const SIXTEEN_BY_NINE_LANDSCAPE = 16 / 9; 5 | export const SIXTEEN_BY_NINE_PORTRAIT = 9 / 16; 6 | export const SQUARE = 1; 7 | 8 | export const CANVAS_WIDTH_LANDSCAPE = VIDEO_WIDTH; 9 | export const CANVAS_HEIGHT_LANDSCAPE = VIDEO_HEIGHT; 10 | 11 | export const CANVAS_WIDTH_PORTRAIT = VIDEO_HEIGHT * SIXTEEN_BY_NINE_PORTRAIT; 12 | export const CANVAS_HEIGHT_PORTRAIT = VIDEO_HEIGHT; 13 | 14 | export const CANVAS_WIDTH_SQUARE = VIDEO_HEIGHT; 15 | export const CANVAS_HEIGHT_SQUARE = VIDEO_HEIGHT; 16 | 17 | function getBaseUrl() { 18 | if (typeof window !== "undefined") 19 | // browser should use relative path 20 | return ""; 21 | if (process.env.VERCEL_URL) 22 | // reference for vercel.com 23 | return `https://${process.env.VERCEL_URL}`; 24 | if (process.env.RENDER_INTERNAL_HOSTNAME) 25 | // reference for render.com 26 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 27 | // assume localhost 28 | return `http://localhost:${process.env.PORT ?? 3000}`; 29 | } 30 | 31 | export const db = { 32 | dbUrl: process.env.DB_URL, 33 | dbName: "logo-markup", 34 | baseUrl: getBaseUrl(), 35 | } as const; 36 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-tag", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^17.0.0", 12 | "@types/react-dom": "^17.0.0", 13 | "axios": "^0.21.1", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-player": "^2.9.0", 17 | "react-scripts": "4.0.3", 18 | "typescript": "^4.3.5", 19 | "web-vitals": "^1.0.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": {} 46 | } 47 | -------------------------------------------------------------------------------- /dogfood/logo-markup/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dogfood/logo-markup/components/TimelinePlayer.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@chakra-ui/react"; 2 | import { TimelineState } from "@xzdarcy/react-timeline-editor"; 3 | import React, { FC, useEffect, useState } from "react"; 4 | 5 | const timeRender = (time: number) => { 6 | const float = (parseInt((time % 1) * 100 + "") + "").padStart(2, "0"); 7 | const min = (parseInt(time / 60 + "") + "").padStart(2, "0"); 8 | const second = (parseInt((time % 60) + "") + "").padStart(2, "0"); 9 | return `${min}:${second}.${float.replace("0.", "")}`; 10 | }; 11 | 12 | const TimelinePlayer: FC<{ 13 | timelineState: React.MutableRefObject; 14 | duration: number; 15 | }> = ({ timelineState, duration }) => { 16 | const [time, setTime] = useState(0); 17 | 18 | useEffect(() => { 19 | if (!timelineState.current) return; 20 | const engine = timelineState.current; 21 | engine.listener.on("afterSetTime", ({ time }) => setTime(time)); 22 | engine.listener.on("setTimeByTick", ({ time }) => { 23 | setTime(time); 24 | }); 25 | 26 | return () => { 27 | if (!engine) return; 28 | engine.pause(); 29 | engine.listener.offAll(); 30 | }; 31 | }, []); 32 | 33 | return ( 34 |
35 | {`${timeRender(time)} | ${timeRender(duration)}`} 36 |
37 | ); 38 | }; 39 | 40 | export default TimelinePlayer; 41 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.tsx`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /dogfood/logo-markup/pages/api/video.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import Video from "@/db/models/Video"; 4 | import { findAllVideos, findVideo, updateVideo } from "../../db/actions/Video"; 5 | import meta from "../../meta/meta.json"; 6 | import dbConnect from "@/db/dbConnect"; 7 | 8 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 9 | if (req.method === "GET") { 10 | if (req.query.videoId) { 11 | get(req, res); 12 | } else { 13 | getAll(req, res); 14 | // test(); 15 | } 16 | } else if (req.method === "PUT") { 17 | update(req, res); 18 | } 19 | } 20 | 21 | async function get(req: NextApiRequest, res: NextApiResponse) { 22 | const { videoId } = req.query; 23 | const video = await findVideo(videoId as string); 24 | return res.status(200).json(video); 25 | } 26 | 27 | async function getAll(req: NextApiRequest, res: NextApiResponse) { 28 | const videos = await findAllVideos(); 29 | return res.status(200).json(videos); 30 | } 31 | 32 | async function update(req: NextApiRequest, res: NextApiResponse) { 33 | const { videoId } = req.query; 34 | try { 35 | const video = await updateVideo(videoId as string, JSON.parse(req.body)); 36 | return res.status(200).json({}); 37 | } catch (e) { 38 | return res.status(400).json({}); 39 | } 40 | } 41 | 42 | async function test() { 43 | await dbConnect(); 44 | try { 45 | const videos = await Promise.all( 46 | meta.map(async (meta, i) => { 47 | const video = new Video({ 48 | id: meta.id, 49 | actions: [], 50 | completed: false, 51 | }); 52 | 53 | await video.save(); 54 | 55 | return video; 56 | }) 57 | ); 58 | } catch (e) { 59 | console.error(e); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dogfood/logo-markup/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/store/index.ts: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "next-i18next"; 2 | import { useContext } from "react"; 3 | import { atom, selector } from "recoil"; 4 | import { combineFilters, tagCategories } from "../components/FilterData"; 5 | import LocaleContext from "../contexts/LocaleContext"; 6 | import meta from "../public/meta.json"; 7 | import { SortOption, TagOption, VideoMeta } from "../types/types"; 8 | 9 | const dateRangeState = atom({ 10 | key: "dateRangeState", 11 | default: ["", ""] as [string, string], 12 | }); 13 | 14 | const listDataState = atom({ 15 | key: "listDataState", 16 | default: meta as VideoMeta[], 17 | }); 18 | 19 | const tagsState = atom({ 20 | key: "tagsState", 21 | default: [] as TagOption[], 22 | }); 23 | 24 | const searchState = atom({ 25 | key: "searchState", 26 | default: "", 27 | }); 28 | 29 | const sortState = atom({ 30 | key: "sortState", 31 | // default: { value: "desc", label: "Most to least recent" } as SortOption, 32 | default: "desc" as "desc" | "asc", 33 | }); 34 | 35 | const renderNumState = atom({ 36 | key: "renderNumState", 37 | default: 20, 38 | }); 39 | 40 | const filteredListState = selector({ 41 | key: "filteredListState", 42 | get: ({ get }) => { 43 | const dateRange = get(dateRangeState); 44 | const search = get(searchState); 45 | const sort = get(sortState); 46 | const tags = get(tagsState); 47 | const listData = get(listDataState); 48 | const renderNum = get(renderNumState); 49 | const filteredList = combineFilters( 50 | [...listData], 51 | dateRange, 52 | sort, 53 | search, 54 | tags 55 | ); 56 | return { 57 | filteredList: filteredList.slice(0, renderNum), 58 | totalResults: filteredList.length, 59 | }; 60 | }, 61 | }); 62 | 63 | export { 64 | dateRangeState, 65 | tagsState, 66 | listDataState, 67 | searchState, 68 | sortState, 69 | renderNumState, 70 | filteredListState, 71 | }; 72 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | preprocessing/ 3 | .idea/ 4 | /yarn.lock 5 | /package.json 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.production 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | .vscode 118 | 119 | # yarn v2 120 | .yarn/cache 121 | .yarn/unplugged 122 | .yarn/build-state.yml 123 | .yarn/install-state.gz 124 | .pnp.* 125 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/pages/pref.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations"; 3 | import Head from "next/head"; 4 | import AutoplayContext from "../contexts/AutoplayContext"; 5 | import { 6 | Box, 7 | Center, 8 | Flex, 9 | Heading, 10 | Switch, 11 | Tag, 12 | Select, 13 | Text, 14 | } from "@chakra-ui/react"; 15 | import { Stack } from "@chakra-ui/react"; 16 | import { useTranslation } from "next-i18next"; 17 | import LocaleContext from "../contexts/LocaleContext"; 18 | import { useRouter } from "next/router"; 19 | 20 | const Pref: React.FC = () => { 21 | const { t, i18n } = useTranslation("pref"); 22 | const { autoplay, toggleAutoplay } = useContext(AutoplayContext); 23 | const { changeLocale } = useContext(LocaleContext); 24 | const router = useRouter(); 25 | 26 | return ( 27 | 28 | 29 | Preferences 30 | 31 |
32 | 38 | {t("heading")} 39 | 40 | 46 | 47 | {t("language.label")} 48 | 49 | 50 | 63 | 64 | 65 | 66 | 67 | {t("autoplay.label")} 68 | 69 | {t("autoplay.description")} 70 | 71 | 72 | 78 | 79 | 80 | 81 |
82 |
83 | ); 84 | }; 85 | 86 | export async function getStaticProps({ locale }: { locale: string }) { 87 | return { 88 | props: { 89 | ...(await serverSideTranslations(locale, ["main", "pref"])), 90 | }, 91 | }; 92 | } 93 | 94 | export default Pref; 95 | -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Router, { NextRouter, useRouter } from "next/router"; 3 | import nprogress from "nprogress"; 4 | import { RecoilRoot } from "recoil"; 5 | import { appWithTranslation, useTranslation } from "next-i18next"; 6 | import nextI18NextConfig from "../next-i18next.config.js"; 7 | import { ChakraProvider, extendTheme } from "@chakra-ui/react"; 8 | import "bootstrap/dist/css/bootstrap.min.css"; 9 | import "../styles/nprogress.css"; 10 | import Head from "next/head"; 11 | import AutoplayContext from "../contexts/AutoplayContext"; 12 | import CustomNav from "../components/CustomNav"; 13 | import LocaleContext from "../contexts/LocaleContext"; 14 | import { serverSideTranslations } from "next-i18next/serverSideTranslations"; 15 | import Maintenance from "../components/Maintenance"; 16 | 17 | const theme = extendTheme({ 18 | colors: { 19 | brand: { 100: "#f8e1eb", 200: "#F0BCD3", 500: "#DB679A" }, 20 | }, 21 | }); 22 | 23 | Router.events.on("routeChangeStart", () => nprogress.start()); 24 | Router.events.on("routeChangeComplete", () => nprogress.done()); 25 | Router.events.on("routeChangeError", () => nprogress.done()); 26 | 27 | interface AppProps { 28 | Component: any; 29 | pageProps: { 30 | [key: string]: any; 31 | }; 32 | } 33 | 34 | const App: React.FC = ({ Component, pageProps }: AppProps) => { 35 | const { i18n } = useTranslation(); 36 | const router = useRouter(); 37 | const [autoplay, setAutoplay] = useState(true); 38 | const [locale, setLocale] = useState(i18n.language); 39 | 40 | const toggleAutoplay = () => { 41 | localStorage.setItem("autoplay", (!autoplay).toString()); 42 | setAutoplay(!autoplay); 43 | }; 44 | 45 | const changeLocale = () => { 46 | const newLocale = locale === "en" ? "ko" : "en"; 47 | setLocale(newLocale); 48 | i18n.changeLanguage(newLocale); 49 | router.push(router.pathname, router.asPath, { 50 | locale: newLocale, 51 | }); 52 | }; 53 | 54 | useEffect(() => { 55 | localStorage.getItem("autoplay") === null 56 | ? localStorage.setItem("autoplay", "true") 57 | : setAutoplay(localStorage.getItem("autoplay") === "true"); 58 | }, []); 59 | 60 | if (process.env.NEXT_PUBLIC_MAINTENANCE === "true") { 61 | return ; 62 | } 63 | 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export async function getStaticProps({ locale }: { locale: string }) { 86 | return { 87 | props: { 88 | ...(await serverSideTranslations(locale, ["main"])), 89 | }, 90 | }; 91 | } 92 | 93 | export default appWithTranslation(App); 94 | -------------------------------------------------------------------------------- /dogfood/logo-markup/components/Rectangle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { Stage, Layer, Rect, Transformer } from "react-konva"; 3 | 4 | interface RectangleProps { 5 | shapeProps: any; 6 | isSelected: boolean; 7 | canvasWidth: number; 8 | canvasHeight: number; 9 | onSelect: (x: any) => void; 10 | onChange: (x: any) => void; 11 | } 12 | 13 | const Rectangle = ({ 14 | shapeProps, 15 | isSelected, 16 | onSelect, 17 | onChange, 18 | canvasWidth, 19 | canvasHeight, 20 | }: RectangleProps) => { 21 | const shapeRef = useRef(); 22 | const trRef = useRef(); 23 | 24 | useEffect(() => { 25 | if (isSelected) { 26 | // we need to attach transformer manually 27 | trRef.current.nodes([shapeRef.current]); 28 | trRef.current.getLayer().batchDraw(); 29 | } 30 | }, [isSelected]); 31 | 32 | return ( 33 | 34 | { 42 | const stage = e.target.getStage(); 43 | const x = Math.max( 44 | 0, 45 | Math.min(stage!.width() - e.target.width(), e.target.x()) 46 | ); 47 | const y = Math.max( 48 | 0, 49 | Math.min(stage!.height() - e.target.height(), e.target.y()) 50 | ); 51 | e.target.position({ x, y }); 52 | }} 53 | onDragEnd={(e) => { 54 | onChange({ 55 | ...shapeProps, 56 | x: e.target.x(), 57 | y: e.target.y(), 58 | }); 59 | }} 60 | onTransformEnd={(e) => { 61 | // transformer is changing scale of the node 62 | // and NOT its width or height 63 | // but in the store we have only width and height 64 | // to match the data better we will reset scale on transform end 65 | const node = shapeRef.current; 66 | const scaleX = node.scaleX(); 67 | const scaleY = node.scaleY(); 68 | 69 | // we will reset it back 70 | node.scaleX(1); 71 | node.scaleY(1); 72 | onChange({ 73 | // ...shapeProps, 74 | x: node.x(), 75 | y: node.y(), 76 | // set minimal value 77 | width: Math.max(5, node.width() * scaleX), 78 | height: Math.max(node.height() * scaleY), 79 | }); 80 | }} 81 | /> 82 | {isSelected && ( 83 | { 88 | const isOut = 89 | newBox.x < 0 || 90 | newBox.y < 0 || 91 | newBox.x + newBox.width > canvasWidth || 92 | newBox.y + newBox.height > canvasHeight; 93 | 94 | if (isOut) { 95 | return oldBox; 96 | } 97 | return newBox; 98 | }} 99 | /> 100 | )} 101 | 102 | ); 103 | }; 104 | 105 | export default Rectangle; 106 | -------------------------------------------------------------------------------- /client/components/VideoCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { VideoMeta } from "../types/types"; 3 | import Link from "next/link"; 4 | import { Box, Link as ChakraLink, Stack, Tag, Text } from "@chakra-ui/react"; 5 | import Image from "next/image"; 6 | import dayjs from "dayjs"; 7 | import "dayjs/locale/en"; 8 | import "dayjs/locale/ko"; 9 | import { useTranslation } from "next-i18next"; 10 | import LocaleContext from "../contexts/LocaleContext"; 11 | 12 | interface VideoCardProps { 13 | item: VideoMeta; 14 | } 15 | 16 | const Duration = ({ duration }: { duration: number }) => { 17 | let h = Math.floor(duration / 3600); 18 | let m = Math.floor((duration % 3600) / 60); 19 | let s = Math.floor(duration % 60); 20 | let hDisplay = h > 0 ? "0" + h + ":" : ""; 21 | let mDisplay = m < 10 ? "0" + m + ":" : m + ":"; 22 | let sDisplay = s < 10 ? "0" + s : s; 23 | return <>{hDisplay + mDisplay + sDisplay}; 24 | }; 25 | 26 | const VideoCard: React.FC = ({ item }) => { 27 | const { t } = useTranslation("filter"); 28 | const { locale } = useContext(LocaleContext); 29 | const { id, thumbnail, title, date, duration, tags, koTitle } = item; 30 | 31 | return ( 32 | 40 | 41 | 49 | 56 | 57 | {Duration({ duration })} 58 | 59 | 60 | 61 | 62 | 63 | 72 | {locale === "en" ? title : koTitle || title} 73 | 74 | 75 | 76 | {locale === "en" 77 | ? dayjs 78 | .utc(date) 79 | .locale(locale) 80 | .local() 81 | .format("MMMM D YYYY, h:mm:ss A") 82 | : dayjs 83 | .utc(date) 84 | .locale(locale) 85 | .local() 86 | .format("YYYY년 MMMM D일 A h:mm:ss")} 87 | 88 | 89 | {tags.map((tag) => ( 90 | 91 | {t("tags." + tag)} 92 | 93 | ))} 94 | 95 | 96 | 97 | ); 98 | }; 99 | 100 | export default VideoCard; 101 | -------------------------------------------------------------------------------- /dogfood/logo-markup/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { 3 | Box, 4 | Button, 5 | Flex, 6 | Heading, 7 | Spinner, 8 | Stack, 9 | Tag, 10 | Text, 11 | } from "@chakra-ui/react"; 12 | import React, { useState } from "react"; 13 | import { GetServerSideProps, InferGetServerSidePropsType } from "next"; 14 | import { db } from "@/utils/consts"; 15 | import { IVideo } from "@/db/models/Video"; 16 | import Link from "next/link"; 17 | 18 | const { baseUrl } = db; 19 | 20 | export default function Home({ 21 | videos, 22 | }: InferGetServerSidePropsType) { 23 | const [loading, setLoading] = useState(false); 24 | const [data, setData] = useState(videos); 25 | return ( 26 | <> 27 | 28 | Video Markup 29 | 30 | 31 | Video Markup Dashboard 32 | 33 | Thanks for making it over to this site! Your help is very very 34 | appreciated - it would take me a while to do this alone. This 35 | dashboard allows you to draw boxes on parts of videos using a timeline 36 | editor. Create boxes by double-clicking on the timeline. Resize the 37 | boxes to cover all logos and trademarked names. While the boxes have 38 | no fill in the editor, be assured that the reprocessed videos will 39 | have the boxes filled in. Once you are done editing a video, feel free 40 | to mark as complete to update the status of the video. I apologize for 41 | the quality of the website - you may run into some issues with the 42 | editor. In this case, please do not hesitate to reach out. Thank you 43 | all so much for your help! 44 | 45 | 59 | 60 | {!loading ? ( 61 | 62 | {data.map((video, i) => { 63 | return ( 64 | 65 | 73 | {video.id} 74 | 75 | {video.completed ? "Completed" : "Incomplete"} 76 | 77 | 78 | 79 | ); 80 | })} 81 | 82 | ) : ( 83 | 84 | )} 85 | 86 | 87 | 88 | ); 89 | } 90 | 91 | export const getServerSideProps: GetServerSideProps<{ 92 | videos: IVideo[]; 93 | }> = async () => { 94 | const res = await fetch(`${baseUrl}/api/video`); 95 | const videos = await res.json(); 96 | return { props: { videos } }; 97 | }; 98 | -------------------------------------------------------------------------------- /client/pages/information.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations"; 3 | import Head from "next/head"; 4 | import { 5 | Text, 6 | Box, 7 | Heading, 8 | Tag, 9 | Link, 10 | Center, 11 | Accordion, 12 | AccordionItem, 13 | AccordionButton, 14 | AccordionPanel, 15 | AccordionIcon, 16 | Icon, 17 | Stack, 18 | Flex, 19 | } from "@chakra-ui/react"; 20 | import { useTranslation } from "next-i18next"; 21 | import { BsGithub } from "react-icons/bs"; 22 | 23 | const Information: React.FC = () => { 24 | const { t } = useTranslation("information"); 25 | return ( 26 | 27 | 28 | Information 29 | 30 | 31 |
32 | 39 | {t("heading")} 40 | 41 | 42 | {t("faq.heading")} 43 | 44 | 45 | 46 | 47 |

48 | 49 | 50 | {t("faq.1.q")} 51 | 52 | 53 | 54 |

55 | 61 | {t("faq.1.a")} 62 | 63 |
64 | 65 | 66 |

67 | 68 | 69 | {t("faq.2.q")} 70 | 71 | 72 | 73 |

74 | 80 | {t("faq.2.a")} 81 | 82 |
83 |
84 |
85 | 86 | 87 | {t("issues.heading")} 88 | 89 | 90 | {t("issues.description")} 91 | 92 | 93 | {t("misc.heading")} 94 | 95 | 96 | {t("misc.description")} 97 | 98 |
99 |
100 | 110 | 111 | 112 |
113 |
114 |
115 |
116 | ); 117 | }; 118 | 119 | export async function getStaticProps({ locale }: { locale: string }) { 120 | return { 121 | props: { 122 | ...(await serverSideTranslations(locale, ["main", "information"])), 123 | }, 124 | }; 125 | } 126 | 127 | export default Information; 128 | -------------------------------------------------------------------------------- /client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef, useContext } from "react"; 2 | import { useTranslation, withTranslation } from "next-i18next"; 3 | import { serverSideTranslations } from "next-i18next/serverSideTranslations"; 4 | import { useRecoilState, useRecoilValue } from "recoil"; 5 | import Head from "next/head"; 6 | import FilterData from "../components/FilterData"; 7 | import dayjs from "dayjs"; 8 | import isBetween from "dayjs/plugin/isBetween"; 9 | import utc from "dayjs/plugin/utc"; 10 | import InfiniteScroll from "react-infinite-scroll-component"; 11 | import { filteredListState, renderNumState } from "../store/index"; 12 | import meta from "../public/meta.json"; 13 | import { VideoMeta } from "../types/types"; 14 | import VideoCard from "../components/VideoCard"; 15 | import { 16 | Box, 17 | Button, 18 | Center, 19 | Flex, 20 | Heading, 21 | Stack, 22 | useDisclosure, 23 | IconButton, 24 | Spacer, 25 | } from "@chakra-ui/react"; 26 | import { ChevronUpIcon } from "@chakra-ui/icons"; 27 | 28 | dayjs.extend(isBetween); 29 | dayjs.extend(utc); 30 | 31 | const VideoList: React.FC = () => { 32 | const { t } = useTranslation("main"); 33 | const { filteredList } = useRecoilValue(filteredListState); 34 | 35 | const [renderNum, setRenderNum] = useRecoilState(renderNumState); 36 | 37 | const [buttonVis, setButtonVis] = useState(false); 38 | 39 | const { isOpen, onOpen, onClose } = useDisclosure(); 40 | const filterButtonRef = useRef(null); 41 | 42 | const toggleVisibility = () => { 43 | if (window.pageYOffset > 300) { 44 | setButtonVis(true); 45 | } else { 46 | setButtonVis(false); 47 | } 48 | }; 49 | 50 | const scrollToTop = () => { 51 | window.scrollTo({ 52 | top: 0, 53 | behavior: "smooth", 54 | }); 55 | }; 56 | const fetchNextData = () => { 57 | setRenderNum(renderNum + 20); 58 | }; 59 | 60 | useEffect(() => { 61 | window.addEventListener("scroll", toggleVisibility); 62 | return () => window.removeEventListener("scroll", toggleVisibility); 63 | }, []); 64 | 65 | return ( 66 | <> 67 | 68 | 69 | IZ*ONE VLIVE Archive 70 | 71 | 72 | 76 | 80 | 81 | 82 | 87 | 88 |
89 | 97 | {t("heading")} 98 | 99 | 107 | 108 | 109 | 117 | 118 | {filteredList.map((item) => ( 119 | 120 | 121 | 122 | ))} 123 | 124 | 125 | 126 |
127 | 128 | {buttonVis ? ( 129 | } 139 | /> 140 | ) : null} 141 |
142 | 143 | ); 144 | }; 145 | 146 | export async function getStaticProps({ locale }: { locale: string }) { 147 | return { 148 | props: { 149 | ...(await serverSideTranslations(locale, ["main", "filter"])), 150 | }, 151 | }; 152 | } 153 | 154 | export default VideoList; 155 | -------------------------------------------------------------------------------- /client/components/CustomNav.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import LocaleContext from "../contexts/LocaleContext"; 3 | import Link from "next/link"; 4 | import { NextRouter, useRouter } from "next/router"; 5 | import { 6 | Box, 7 | Flex, 8 | Text, 9 | IconButton, 10 | Image, 11 | Button, 12 | Stack, 13 | Collapse, 14 | Icon, 15 | Link as ChakraLink, 16 | useColorModeValue, //used for dark mode (future?) 17 | useDisclosure, 18 | } from "@chakra-ui/react"; 19 | import { 20 | HamburgerIcon, 21 | CloseIcon, 22 | SettingsIcon, 23 | InfoIcon, 24 | } from "@chakra-ui/icons"; 25 | import { BiDonateHeart } from "react-icons/bi"; 26 | import { useTranslation, TFunction, I18n } from "next-i18next"; 27 | 28 | const CustomNav: React.FC = () => { 29 | const router = useRouter(); 30 | const { t, i18n } = useTranslation("main"); 31 | const { isOpen, onToggle } = useDisclosure(); 32 | const { changeLocale } = useContext(LocaleContext); 33 | 34 | return ( 35 | 36 | 46 | 47 | : 51 | } 52 | variant={"unstyled"} 53 | bg="gray.800" 54 | _hover={{ color: "gray.400", transition: "0.3s" }} 55 | color="gray.500" 56 | aria-label={"Toggle Navigation"} 57 | /> 58 | 59 | 64 | 65 | 71 | 72 | 73 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | const DesktopNav = ({ 95 | locale, 96 | changeLocale, 97 | }: { 98 | locale: string; 99 | changeLocale: () => void; 100 | router: NextRouter; 101 | }) => { 102 | return ( 103 | 104 | 114 | {locale} 115 | 116 | 117 | 128 | 129 | 134 | 146 | 147 | 148 | 159 | 160 | 161 | ); 162 | }; 163 | 164 | const MobileMenu = ({ t }: { t: TFunction }) => { 165 | return ( 166 | 167 | 168 | 174 | {t("information")} 175 | 176 | 177 | 182 | 183 | {t("donate")} 184 | 185 | 186 | 187 | 193 | {t("preferences")} 194 | 195 | 196 | 197 | ); 198 | }; 199 | 200 | export default CustomNav; 201 | -------------------------------------------------------------------------------- /client/pages/video/[id].tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { serverSideTranslations } from "next-i18next/serverSideTranslations"; 3 | import Link from "next/link"; 4 | import Head from "next/head"; 5 | import dayjs from "dayjs"; 6 | import "dayjs/locale/en"; 7 | import "dayjs/locale/ko"; 8 | import utc from "dayjs/plugin/utc"; 9 | import ReactPlayer from "react-player/lazy"; 10 | import meta from "../../public/meta.json"; 11 | import { VideoMeta } from "../../types/types"; 12 | import AutoplayContext from "../../contexts/AutoplayContext"; 13 | import { 14 | Box, 15 | Center, 16 | Link as ChakraLink, 17 | Stack, 18 | Tag, 19 | Text, 20 | } from "@chakra-ui/react"; 21 | import { ArrowBackIcon } from "@chakra-ui/icons"; 22 | import { useTranslation } from "next-i18next"; 23 | import LocaleContext from "../../contexts/LocaleContext"; 24 | 25 | dayjs.extend(utc); 26 | 27 | const Video: React.FC<{ vidObj: VideoMeta }> = ({ vidObj }) => { 28 | const { t } = useTranslation("video"); 29 | const { autoplay } = useContext(AutoplayContext); 30 | const { locale } = useContext(LocaleContext); 31 | const { date, title, video, thumbnail, subtitles, koTitle } = vidObj; 32 | 33 | return ( 34 | 35 | 36 | {title} 37 | 38 | 39 | 40 | 44 | 48 | 49 | 50 |
51 | 52 | 53 | 62 | 63 | 64 | 70 | 71 | ({ 85 | kind: "subtitles", 86 | src: e.url, 87 | srcLang: e.code, 88 | label: e.code, 89 | })), 90 | }, 91 | }} 92 | /> 93 | 94 | 95 | 96 | 97 | {locale === "en" ? title : koTitle || title} 98 | 99 | 100 | {locale === "en" 101 | ? dayjs 102 | .utc(date) 103 | .locale(locale) 104 | .local() 105 | .format("MMMM D YYYY, h:mm:ss A") 106 | : dayjs 107 | .utc(date) 108 | .locale(locale) 109 | .local() 110 | .format("YYYY년 MMMM D일 A h:mm:ss")} 111 | 112 | {subtitles.length !== 0 ? ( 113 |
122 | 123 | {t("subtitleMsg")} 124 | 125 | {subtitles.map(({ code }) => ( 126 | 127 | {code} 128 | 129 | ))} 130 | 131 | 132 |
133 | ) : null} 134 |
135 |
136 |
137 |
138 |
139 |
140 | ); 141 | }; 142 | 143 | export async function getStaticProps({ 144 | params, 145 | locale, 146 | }: { 147 | params: { id: string }; 148 | locale: string; 149 | }) { 150 | const vidObj = meta.filter((vid) => vid.id === params.id)[0]; 151 | return { 152 | props: { 153 | vidObj, 154 | ...(await serverSideTranslations(locale, ["main", "video"])), 155 | }, 156 | }; 157 | } 158 | 159 | export async function getStaticPaths() { 160 | const paths = meta.flatMap((video) => [ 161 | { 162 | params: { 163 | id: video.id, 164 | }, 165 | locale: "en", 166 | }, 167 | { 168 | params: { 169 | id: video.id, 170 | }, 171 | locale: "ko", 172 | }, 173 | ]); 174 | return { paths, fallback: false }; 175 | } 176 | 177 | export default Video; 178 | -------------------------------------------------------------------------------- /dogfood/video-tag/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useEffect, useState } from "react"; 2 | import ReactPlayer from "react-player"; 3 | import axios from "axios"; 4 | import "./App.css"; 5 | 6 | type VideoMeta = { 7 | id: string; 8 | date: string; 9 | title: string; 10 | duration: number; 11 | video: string; 12 | thumbnail: string; 13 | subtitles: SubtitleMeta[]; 14 | tags: string[]; 15 | }; 16 | 17 | type SubtitleMeta = { 18 | code: string; 19 | url: string; 20 | }; 21 | 22 | //? TAGS: Live, VPICK, ENOZI, Arcade, Promotion, MV, Dance Practice, Misc 23 | 24 | const App: React.FC = () => { 25 | const tags = [ 26 | "Live", 27 | "VPICK", 28 | "ENOZI", 29 | "Promotion", 30 | "MV", 31 | "Cheer Guide", 32 | "Making Film", 33 | "LieV", 34 | "Star Road", 35 | "Idol Room", 36 | "Greetings", 37 | "Dance Practice", 38 | "Audio Only", 39 | "Misc", 40 | ]; 41 | 42 | const [videoMeta, setVideoMeta] = useState([]); 43 | const [index, setIndex] = useState(0); 44 | const [applyState, setApplyState] = useState(false); //true: unsaved changes 45 | const [goToInput, setGoToInput] = useState(""); 46 | 47 | useEffect(() => { 48 | axios.get("http://localhost:5000/").then((resp) => { 49 | setVideoMeta(resp.data); 50 | }); 51 | }, []); 52 | 53 | const tagAmount = useMemo(() => { 54 | return videoMeta.filter((e) => e.tags.length !== 0).length; 55 | }, [videoMeta]); 56 | 57 | return ( 58 |
59 |

{`Total videos tagged: ${tagAmount}/554`}

60 | {videoMeta.length !== 0 ? ( 61 |
69 |
70 | ({ 83 | kind: "subtitles", 84 | src: e.url, 85 | srcLang: e.code, 86 | label: e.code, 87 | })), 88 | }, 89 | }} 90 | /> 91 |
92 |

{videoMeta[index].title}

93 |
94 | {tags.map((tag) => ( 95 | 122 | ))} 123 |
124 |

{`${videoMeta[index].date} ${videoMeta[index].id}`}

125 |
126 | ) : null} 127 |
128 | 139 | 150 |
151 | 163 |
164 | Skip to next: 165 | {tags.map((e) => ( 166 | 178 | ))} 179 |
180 |
181 | Go to video: 182 | setGoToInput(e.target.value)} 185 | /> 186 | 196 |
197 | {applyState ? ( 198 |
199 |

Unsaved changes

200 |
201 | 210 |
211 |
212 | ) : null} 213 |
214 | ); 215 | }; 216 | 217 | export default App; 218 | -------------------------------------------------------------------------------- /client/components/FilterData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilState, useRecoilValue } from "recoil"; 3 | import dayjs from "dayjs"; 4 | import { SortOption, TagOption, VideoMeta } from "../types/types"; 5 | import { 6 | filteredListState, 7 | dateRangeState, 8 | sortState, 9 | searchState, 10 | tagsState, 11 | } from "../store"; 12 | import { 13 | Input, 14 | Drawer, 15 | DrawerBody, 16 | DrawerCloseButton, 17 | DrawerContent, 18 | DrawerHeader, 19 | DrawerOverlay, 20 | Stack, 21 | Box, 22 | Text, 23 | Button, 24 | } from "@chakra-ui/react"; 25 | 26 | import Select from "react-select"; 27 | import customParseFormat from "dayjs/plugin/customParseFormat"; 28 | import { useTranslation } from "next-i18next"; 29 | dayjs.extend(customParseFormat); 30 | 31 | const combineFilters = ( 32 | data: VideoMeta[], 33 | dateRange: [string, string], 34 | sort: "desc" | "asc", 35 | search: string, 36 | tags: TagOption[] 37 | ) => { 38 | const parsedStartDate = dayjs(dateRange[0], "MM/DD/YYYY", true); 39 | const parsedEndDate = dayjs(dateRange[1], "MM/DD/YYYY", true); 40 | 41 | let filteredListData = [...data]; 42 | 43 | // tags 44 | if (tags.length !== 0) { 45 | // satisfied when VideoMeta item tags includes any user filtered tags (OR) 46 | filteredListData = data.filter((item) => 47 | tags.some((e) => item.tags.includes(e.value)) 48 | ); 49 | } 50 | // date 51 | if (parsedStartDate.isValid() && parsedEndDate.isValid()) { 52 | filteredListData = filteredListData.filter((item) => { 53 | if ( 54 | dayjs 55 | .utc(item.date) 56 | .local() 57 | .isBetween(parsedStartDate, parsedEndDate, "day", "[]") 58 | ) { 59 | return true; 60 | } 61 | }); 62 | } 63 | 64 | // search 65 | if (search !== "") { 66 | filteredListData = filteredListData.filter( 67 | (item) => 68 | item.koTitle?.toLowerCase().includes(search) || 69 | item.title.toLowerCase().includes(search) 70 | ); 71 | } 72 | 73 | // sort 74 | if (sort === "asc") { 75 | filteredListData = filteredListData.sort( 76 | (a, b) => dayjs(a.date).unix() - dayjs(b.date).unix() 77 | ); 78 | } else { 79 | filteredListData = filteredListData.sort( 80 | (a, b) => dayjs(b.date).unix() - dayjs(a.date).unix() 81 | ); 82 | } 83 | 84 | return filteredListData; 85 | }; 86 | 87 | interface FilterDataProps { 88 | filterButtonRef: React.RefObject; 89 | isOpen: boolean; 90 | onClose: () => void; 91 | } 92 | 93 | const FilterData: React.FC = ({ 94 | filterButtonRef, 95 | isOpen, 96 | onClose, 97 | }) => { 98 | const { t } = useTranslation("filter"); 99 | const { totalResults } = useRecoilValue(filteredListState); 100 | const [dateRange, setDateRange] = useRecoilState(dateRangeState); 101 | const [startDate, endDate] = dateRange; 102 | const [sort, setSort] = useRecoilState(sortState); 103 | const [tags, setTags] = useRecoilState(tagsState); 104 | const [search, setSearch] = useRecoilState(searchState); 105 | 106 | const resetFilters = () => { 107 | setDateRange(["", ""]); 108 | // setSort({ value: "desc", label: "Most to least recent" }); 109 | setSort("desc"); 110 | setTags([]); 111 | setSearch(""); 112 | }; 113 | 114 | const sortOptions: SortOption[] = [ 115 | { 116 | value: "desc", 117 | label: t("desc"), 118 | }, 119 | { 120 | value: "asc", 121 | label: t("asc"), 122 | }, 123 | ]; 124 | 125 | const tagOptions: TagOption[] = [ 126 | { label: t("tags.live"), value: "live" }, 127 | { label: t("tags.deleted"), value: "deleted" }, 128 | { label: t("tags.vpick"), value: "vpick" }, 129 | { label: t("tags.enozi"), value: "enozi" }, 130 | { label: t("tags.promotion"), value: "promotion" }, 131 | { label: t("tags.mv"), value: "mv" }, 132 | { label: t("tags.cheerGuide"), value: "cheerGuide" }, 133 | { label: t("tags.makingFilm"), value: "makingFilm" }, 134 | { label: t("tags.liev"), value: "liev" }, 135 | { label: t("tags.starRoad"), value: "starRoad" }, 136 | { label: t("tags.idolRoom"), value: "idolRoom" }, 137 | { label: t("tags.greetings"), value: "greetings" }, 138 | { label: t("tags.dancePractice"), value: "dancePractice" }, 139 | { label: t("tags.audioOnly"), value: "audioOnly" }, 140 | { label: t("tags.misc"), value: "misc" }, 141 | ]; 142 | 143 | return ( 144 | 150 | 151 | 152 | 153 | {`${t("videosFound")}${totalResults}`} 154 | 155 | 156 | 157 | 162 | setDateRange([event.target.value, endDate]) 163 | } 164 | /> 165 | 170 | setDateRange([startDate, event.target.value]) 171 | } 172 | /> 173 | 174 | setSearch(event.target.value)} 201 | /> 202 |