├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── README.md ├── deploy.ts ├── destroy.ts ├── get-rate-limits.ts ├── list-emails.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [user] │ └── index.tsx ├── _app.tsx ├── about.tsx ├── api │ ├── email.ts │ ├── progress.ts │ ├── render.ts │ ├── stats │ │ └── [user].ts │ └── videos.ts ├── dashboard.tsx └── index.tsx ├── public ├── Mona-Sans-Black.otf ├── Mona-Sans-Bold.otf ├── Mona-Sans-ExtraBold.otf ├── Mona-Sans-Medium.otf ├── fav.png ├── favicons │ ├── blue.png │ ├── golden.png │ ├── green.png │ └── red.png ├── flash.png ├── music │ ├── track-1.mp3 │ ├── track-1.wav │ ├── track-2.mp3 │ ├── track-2.wav │ ├── track-3.mp3 │ └── track-3.wav ├── promo1.png ├── promo2.png ├── sound.mp3 ├── star.png ├── vercel.svg └── watercolour.png ├── remotion ├── AnimatedCommit.tsx ├── AnimatedTopPullRequest.tsx ├── Arc.tsx ├── AvatarFrame.tsx ├── AvgCommits.tsx ├── AvgCommitsTitle.tsx ├── Band.tsx ├── BestCommits.tsx ├── Bow.tsx ├── Commit.tsx ├── CommitBar.tsx ├── Contribs.tsx ├── EndCard.tsx ├── FeatureList.tsx ├── Flashcard.tsx ├── Gift.tsx ├── GiftBox.tsx ├── Github.tsx ├── GithubComp.tsx ├── GithubPromo.tsx ├── IDidALot.tsx ├── Icons │ ├── Bonbon.tsx │ ├── Candy.tsx │ ├── Gingerman.tsx │ ├── Logo.tsx │ ├── Sock.tsx │ ├── Star.tsx │ └── Tree.tsx ├── IssueCircle.tsx ├── IssuesOpened.tsx ├── LangPlaceholder.tsx ├── LanguageToSocks.tsx ├── Languages │ ├── CMake.tsx │ ├── CPlusPlus.tsx │ ├── Clojure.tsx │ ├── CoffeeScript.tsx │ ├── Css.tsx │ ├── Flutter.tsx │ ├── GraphQl.tsx │ ├── HTML.tsx │ ├── Haskell.tsx │ ├── Java.tsx │ ├── JavaScript.tsx │ ├── Kotlin.tsx │ ├── LanguageIcon.tsx │ ├── Lua.tsx │ ├── Php.tsx │ ├── PowerShell.tsx │ ├── Python.tsx │ ├── RLang.tsx │ ├── Reason.tsx │ ├── Ruby.tsx │ ├── Rust.tsx │ ├── SQL.tsx │ ├── Sass.tsx │ ├── Scala.tsx │ ├── Solidity.tsx │ ├── Swift.tsx │ ├── Typescript.tsx │ └── Vue.tsx ├── Laptop.tsx ├── Loader.tsx ├── LoadingPage.tsx ├── Main.tsx ├── MiddleLine.tsx ├── Moon.tsx ├── NoIssues.tsx ├── PromoGiftBox.tsx ├── PromoTitle.tsx ├── Rank.tsx ├── Root.tsx ├── RoughCircle.tsx ├── RoughCircleStatic.tsx ├── RoughEllipse.tsx ├── RoughPath.tsx ├── Santa Hat.svg ├── SlideIn.tsx ├── Snow.tsx ├── SockComp.tsx ├── Socks.tsx ├── Squeeze.tsx ├── SunMoon.tsx ├── Teaser.tsx ├── TeaserGift.tsx ├── Title2022.tsx ├── TitleCard.tsx ├── TopLang.tsx ├── TopLangTitle.tsx ├── TopLanguageIcon.tsx ├── TopLanguages.tsx ├── TopPullRequest.tsx ├── TopWeekdays.tsx ├── TotalContributions.tsx ├── TreeComp.tsx ├── TreeGithub.tsx ├── Unwrap.tsx ├── UnwrappedEnd.tsx ├── WallHanger.tsx ├── WeekdayBar.tsx ├── all.ts ├── commits.ts ├── font.ts ├── frontend-stats.ts ├── get-rough.ts ├── github-api.ts ├── index.ts ├── ios-safari.ts ├── language-list.tsx ├── map-api-response-to-commits.ts ├── map-response-to-stats.ts ├── og │ ├── Og.tsx │ ├── StaticRoughPath.tsx │ ├── StaticSnow.tsx │ ├── StaticTree.tsx │ ├── StillAvatarFrame.tsx │ ├── StillTitleCard.tsx │ └── StillWallHanger.tsx ├── rank-commit.ts ├── roughen-path.ts ├── round-svg.ts ├── theme.tsx ├── top-language-stairs.tsx ├── tree │ └── indices-to-close.ts └── use-noise-translate.ts ├── src ├── components │ ├── AnimatedRoughBox.tsx │ ├── ArrowLeft.tsx │ ├── Bauble.tsx │ ├── Bird.tsx │ ├── Camera.tsx │ ├── Download.tsx │ ├── EmailForm.tsx │ ├── Error.tsx │ ├── FaqPage.tsx │ ├── Footer.tsx │ ├── Gingerman.tsx │ ├── Github.tsx │ ├── GithubSquare.tsx │ ├── Grill.tsx │ ├── HomeComponent.tsx │ ├── HomeSidebar.tsx │ ├── Laptop.tsx │ ├── LinkedIn.tsx │ ├── Play.tsx │ ├── Question.tsx │ ├── RoughBox.tsx │ ├── SadGingerman.tsx │ ├── Sun.tsx │ ├── ThemeSwitcher.tsx │ ├── ThemeSwitcherContent.tsx │ ├── ThemeSwitcherItem.tsx │ ├── Unwrapped.tsx │ ├── UserPage.tsx │ └── button.ts ├── config.ts ├── db │ ├── cache.ts │ ├── mongo.ts │ └── renders.ts ├── discord-monitoring.ts ├── format-bytes.ts ├── get-account-count.ts ├── get-all.ts ├── get-random-aws-account.ts ├── get-random-github-token.ts ├── get-render-or-make.ts ├── get-render-progress-with-finality.ts ├── get-times-of-day.ts ├── has-enough-data.ts ├── og-images.ts ├── regions.ts ├── set-env-for-key.ts ├── truthy.ts ├── types.ts └── use-window-size.ts ├── styles └── globals.css ├── test ├── top-hours.test.ts ├── top-languages.test.ts └── tree.test.ts ├── tsconfig.json └── vitest.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | AWS_KEY_1= 2 | AWS_SECRET_1= 3 | AWS_KEY_2= 4 | AWS_SECRET_2= 5 | MONGO_URL= 6 | DISCORD_TOKEN= 7 | DISCORD_CHANNEL= 8 | GITHUB_TOKEN_1= 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .env 36 | tsconfig.tsbuildinfo 37 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | bracketSpacing: false, 4 | useTabs: true, 5 | overrides: [ 6 | { 7 | files: ['*.yml'], 8 | options: { 9 | singleQuote: false, 10 | }, 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": false, 5 | "source.fixAll": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /deploy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | deployFunction, 3 | deploySite, 4 | getOrCreateBucket, 5 | getRegions, 6 | } from '@remotion/lambda'; 7 | import dotenv from 'dotenv'; 8 | import path from 'path'; 9 | import {RAM, SITE_ID, TIMEOUT} from './src/config'; 10 | import {getAccountCount} from './src/get-account-count'; 11 | import {setEnvForKey} from './src/set-env-for-key'; 12 | dotenv.config(); 13 | 14 | const count = getAccountCount(); 15 | console.log(`Found ${count} accounts. Deploying...`); 16 | 17 | const execute = async () => { 18 | for (let i = 1; i <= count; i++) { 19 | for (const region of getRegions()) { 20 | setEnvForKey(i); 21 | const {functionName, alreadyExisted} = await deployFunction({ 22 | architecture: 'arm64', 23 | createCloudWatchLogGroup: true, 24 | memorySizeInMb: RAM, 25 | timeoutInSeconds: TIMEOUT, 26 | region, 27 | }); 28 | console.log( 29 | `${ 30 | alreadyExisted ? 'Ensured' : 'Deployed' 31 | } function "${functionName}" to ${region} in account ${i}` 32 | ); 33 | const {bucketName} = await getOrCreateBucket({region}); 34 | const {serveUrl} = await deploySite({ 35 | siteName: SITE_ID, 36 | bucketName, 37 | entryPoint: path.join(process.cwd(), 'remotion/index.ts'), 38 | region, 39 | }); 40 | console.log( 41 | `Deployed site to ${region} in account ${i} under ${serveUrl}` 42 | ); 43 | } 44 | } 45 | }; 46 | 47 | execute() 48 | .then(() => process.exit(0)) 49 | .catch((err) => { 50 | console.error(err); 51 | process.exit(1); 52 | }); 53 | -------------------------------------------------------------------------------- /destroy.ts: -------------------------------------------------------------------------------- 1 | // Extremely destructive script. Deletes all buckets in your account. 2 | 3 | import {getAwsClient, getOrCreateBucket, getRegions} from '@remotion/lambda'; 4 | import dotenv from 'dotenv'; 5 | import chunk from 'lodash.chunk'; 6 | import {getAccountCount} from './src/get-account-count'; 7 | import {setEnvForKey} from './src/set-env-for-key'; 8 | 9 | dotenv.config(); 10 | const count = getAccountCount(); 11 | console.log(`Found ${count} accounts. Deploying...`); 12 | 13 | const execute = async () => { 14 | for (let i = 1; i <= count; i++) { 15 | for (const region of getRegions()) { 16 | setEnvForKey(i); 17 | 18 | const {client, sdk} = getAwsClient({ 19 | region, 20 | service: 's3', 21 | customCredentials: null, 22 | }); 23 | 24 | const bucket = await getOrCreateBucket({region}); 25 | 26 | const files = await client.send( 27 | new sdk.ListObjectsCommand({ 28 | Bucket: bucket.bucketName, 29 | }) 30 | ); 31 | 32 | const chunks = chunk(files.Contents ?? [], 10); 33 | for (const file of chunks) { 34 | await Promise.all( 35 | file.map((f) => 36 | client.send( 37 | new sdk.DeleteObjectCommand({ 38 | Bucket: bucket.bucketName, 39 | Key: f.Key, 40 | }) 41 | ) 42 | ) 43 | ); 44 | console.log( 45 | 'deleted', 46 | file.map((f) => f.Key), 47 | 'from bucket', 48 | bucket.bucketName, 49 | 'in region', 50 | region, 51 | 'in account', 52 | i 53 | ); 54 | } 55 | 56 | console.log(files.Contents?.length); 57 | 58 | await client.send( 59 | new sdk.DeleteBucketCommand({ 60 | Bucket: bucket.bucketName, 61 | }) 62 | ); 63 | } 64 | } 65 | }; 66 | 67 | execute() 68 | .then(() => process.exit(0)) 69 | .catch((err) => { 70 | console.error(err); 71 | process.exit(1); 72 | }); 73 | -------------------------------------------------------------------------------- /get-rate-limits.ts: -------------------------------------------------------------------------------- 1 | import {getAwsClient, getRegions} from '@remotion/lambda'; 2 | import {LAMBDA_CONCURRENCY_LIMIT_QUOTA} from '@remotion/lambda/dist/defaults'; 3 | import dotenv from 'dotenv'; 4 | 5 | import {getAccountCount} from './src/get-account-count'; 6 | import {setEnvForKey} from './src/set-env-for-key'; 7 | dotenv.config(); 8 | 9 | const count = getAccountCount(); 10 | console.log(`Found ${count} accounts. Getting service quota...`); 11 | 12 | const execute = async () => { 13 | for (let i = 1; i <= count; i++) { 14 | for (const region of getRegions()) { 15 | setEnvForKey(i); 16 | const {client, sdk} = getAwsClient({ 17 | region, 18 | service: 'servicequotas', 19 | }); 20 | 21 | const quota = await client.send( 22 | new sdk.GetServiceQuotaCommand({ 23 | QuotaCode: LAMBDA_CONCURRENCY_LIMIT_QUOTA, 24 | ServiceCode: 'lambda', 25 | }) 26 | ); 27 | 28 | console.log(i, region, quota.Quota?.Value); 29 | 30 | if ((quota.Quota?.Value ?? 0) < 1000) { 31 | console.log(`Quota for ${i} ${region} is not 1000!`); 32 | const openCases = await client.send( 33 | new sdk.ListRequestedServiceQuotaChangeHistoryByQuotaCommand({ 34 | QuotaCode: LAMBDA_CONCURRENCY_LIMIT_QUOTA, 35 | ServiceCode: 'lambda', 36 | }) 37 | ); 38 | const openCase = openCases.RequestedQuotas?.find( 39 | (r) => r.Status === 'CASE_OPENED' 40 | ); 41 | if (openCase) { 42 | console.log('already requested, skipping'); 43 | continue; 44 | } 45 | await client.send( 46 | new sdk.RequestServiceQuotaIncreaseCommand({ 47 | QuotaCode: LAMBDA_CONCURRENCY_LIMIT_QUOTA, 48 | DesiredValue: 1000, 49 | ServiceCode: 'lambda', 50 | }) 51 | ); 52 | console.log('requested increase to 1000'); 53 | } 54 | } 55 | } 56 | }; 57 | 58 | execute() 59 | .then(() => process.exit(0)) 60 | .catch((err) => { 61 | console.error(err); 62 | process.exit(1); 63 | }); 64 | -------------------------------------------------------------------------------- /list-emails.ts: -------------------------------------------------------------------------------- 1 | // Lists all emails that have been submitted on the about page as CSV 2 | 3 | // organize-imports-ignore 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | import {dbEmailCollection} from './src/db/cache'; 7 | 8 | const execute = async () => { 9 | const emails = await (await dbEmailCollection()).find().toArray(); 10 | console.log(emails.map((e) => e.email).join('\n')); 11 | }; 12 | 13 | execute() 14 | .then(() => process.exit(0)) 15 | .catch((err) => { 16 | console.error(err); 17 | process.exit(1); 18 | }); 19 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: false, 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-unwrapped", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "preview": "npx remotion preview remotion/index.tsx", 11 | "render": "npx remotion render remotion/index.tsx main out/unwrapped.mp4", 12 | "deploy-functions": "ts-node deploy.ts" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "@remotion/bundler": "3.3.80", 17 | "@remotion/cli": "3.3.80", 18 | "@remotion/lambda": "3.3.80", 19 | "@remotion/noise": "3.3.80", 20 | "@remotion/paths": "3.3.80", 21 | "@remotion/player": "3.3.80", 22 | "@remotion/renderer": "3.3.80", 23 | "@types/lodash.chunk": "^4.2.6", 24 | "@types/lodash.samplesize": "^4.2.7", 25 | "@types/node-fetch": "^2.6.2", 26 | "cookies-next": "^2.1.1", 27 | "dotenv": "^10.0.0", 28 | "lodash.chunk": "^4.2.0", 29 | "lodash.samplesize": "^4.2.0", 30 | "mongodb": "^4.12.1", 31 | "next": "12.3.1", 32 | "node-fetch": "^2.6.7", 33 | "polished": "^4.1.3", 34 | "prettier": "^2.8.1", 35 | "prettier-plugin-organize-imports": "^3.2.1", 36 | "react": "18.2.0", 37 | "react-dom": "18.2.0", 38 | "remotion": "3.3.80", 39 | "roughjs": "^4.5.2", 40 | "simplex-noise": "3.0.0", 41 | "svg-round-corners": "^0.4.1", 42 | "three": "^0.149.0", 43 | "ts-node": "^10.9.1" 44 | }, 45 | "devDependencies": { 46 | "@types/react": "^17.0.37", 47 | "eslint": "8.4.1", 48 | "eslint-config-next": "12.3.1", 49 | "typescript": "^4.5.2", 50 | "vitest": "^0.25.6" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import {AppProps} from 'next/app'; 2 | import Head from 'next/head'; 3 | import React from 'react'; 4 | import '../styles/globals.css'; 5 | 6 | const MyApp: React.FC = ({Component, pageProps}) => { 7 | return ( 8 | <> 9 | 10 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default MyApp; 21 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import {GetServerSideProps} from 'next'; 2 | import React from 'react'; 3 | import {ThemeProvider} from '../remotion/theme'; 4 | import {getCookie} from 'cookies-next'; 5 | import {FaqPage} from '../src/components/FaqPage'; 6 | 7 | type Props = { 8 | initialTheme: string | null; 9 | }; 10 | 11 | export const getServerSideProps: GetServerSideProps = async ({ 12 | req, 13 | res, 14 | }) => { 15 | return { 16 | props: { 17 | initialTheme: (getCookie('theme', {req, res}) as string) ?? null, 18 | }, 19 | }; 20 | }; 21 | 22 | const Faq: React.FC = ({initialTheme}) => { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Faq; 31 | -------------------------------------------------------------------------------- /pages/api/email.ts: -------------------------------------------------------------------------------- 1 | import {NextApiRequest, NextApiResponse} from 'next'; 2 | import {getEmailFromDb, saveEmailAdress} from '../../src/db/cache'; 3 | import {sendDiscordMessage} from '../../src/discord-monitoring'; 4 | import {NOT_FOUND_TOKEN} from '../../src/get-all'; 5 | import {EmailResponse} from '../../src/types'; 6 | 7 | export default async function handler( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | const email = req.body.email; 12 | if (typeof email !== 'string') { 13 | return res.status(400).json({type: 'error', error: 'No email passed'}); 14 | } 15 | try { 16 | const existingEmail = await getEmailFromDb(email); 17 | if (existingEmail) { 18 | return res.status(201).json({ 19 | type: 'success', 20 | message: 'Your email has already been provided.', 21 | }); 22 | } 23 | sendDiscordMessage(`New email submitted: ${email}`); 24 | await saveEmailAdress(email); 25 | return res 26 | .status(201) 27 | .json({type: 'success', message: 'Your email has been saved!'}); 28 | } catch (err) { 29 | console.log(err); 30 | if ((err as Error).message.includes(NOT_FOUND_TOKEN)) { 31 | return res.status(200).json({ 32 | type: 'error', 33 | error: 'not-found', 34 | }); 35 | } 36 | throw err; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pages/api/progress.ts: -------------------------------------------------------------------------------- 1 | import type {NextApiRequest, NextApiResponse} from 'next'; 2 | import {getRender} from '../../src/db/renders'; 3 | import {getRenderProgressWithFinality} from '../../src/get-render-progress-with-finality'; 4 | import {ProgressData, RenderProgressOrFinality} from '../../src/types'; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | const body = JSON.parse(req.body) as ProgressData; 11 | const render = await getRender({username: body.username, theme: body.theme}); 12 | if (!render) { 13 | throw new Error('Could not get progress for '); 14 | } 15 | 16 | const prog = await getRenderProgressWithFinality({ 17 | render, 18 | assume0Progress: false, 19 | }); 20 | 21 | res.status(200).json(prog); 22 | return; 23 | } 24 | -------------------------------------------------------------------------------- /pages/api/render.ts: -------------------------------------------------------------------------------- 1 | import {NextApiRequest, NextApiResponse} from 'next'; 2 | import {saveCache} from '../../src/db/cache'; 3 | import {sendDiscordMessage} from '../../src/discord-monitoring'; 4 | import {getRenderOrMake} from '../../src/get-render-or-make'; 5 | import {getOgImageOrMake} from '../../src/og-images'; 6 | import {RenderProgressOrFinality, RenderRequest} from '../../src/types'; 7 | 8 | export default async function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | const body = JSON.parse(req.body) as RenderRequest; 13 | const saveCacheProm = saveCache({ 14 | username: body.username, 15 | stats: body.compactStats, 16 | }); 17 | const prog = await getRenderOrMake({ 18 | username: body.username, 19 | stats: body.compactStats, 20 | themeId: body.theme, 21 | }); 22 | 23 | // Trigger og:image render for later 24 | getOgImageOrMake({username: body.username}).catch((err) => { 25 | sendDiscordMessage(`Failed to get og:image: ${err.stack}`); 26 | }); 27 | 28 | res.status(200).json(prog); 29 | await saveCacheProm; 30 | } 31 | -------------------------------------------------------------------------------- /pages/api/stats/[user].ts: -------------------------------------------------------------------------------- 1 | import {NextApiRequest, NextApiResponse} from 'next'; 2 | import {BackendStatsResponse} from '../../../remotion/map-response-to-stats'; 3 | import {backendStatsCollection} from '../../../src/db/cache'; 4 | import { 5 | backendResponseToBackendStats, 6 | getAll, 7 | NOT_FOUND_TOKEN, 8 | } from '../../../src/get-all'; 9 | import {getRandomGithubToken} from '../../../src/get-random-github-token'; 10 | import {hasEnoughBackendData} from '../../../src/has-enough-data'; 11 | 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const username = req.query.user; 17 | if (typeof username !== 'string') { 18 | return res.status(400); 19 | } 20 | const entry = await ( 21 | await backendStatsCollection() 22 | ).findOne({ 23 | username, 24 | }); 25 | 26 | if (entry) { 27 | return res 28 | .status(200) 29 | .json({type: 'found', backendStats: entry.backendStats}); 30 | } 31 | 32 | const response = await getAll(username, getRandomGithubToken()); 33 | 34 | try { 35 | const backendStats = backendResponseToBackendStats(response); 36 | 37 | if (hasEnoughBackendData(backendStats)) { 38 | (await backendStatsCollection()).insertOne({ 39 | backendStats, 40 | username, 41 | }); 42 | } 43 | 44 | return res.status(200).json({type: 'found', backendStats}); 45 | } catch (err) { 46 | if ((err as Error).message.includes(NOT_FOUND_TOKEN)) { 47 | return res.status(200).json({ 48 | type: 'not-found', 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pages/api/videos.ts: -------------------------------------------------------------------------------- 1 | import type {NextApiRequest, NextApiResponse} from 'next'; 2 | import {rendersCollection} from '../../src/db/renders'; 3 | 4 | export default async function handler( 5 | _req: NextApiRequest, 6 | res: NextApiResponse<{renders: number}> 7 | ) { 8 | const renders = await ( 9 | await rendersCollection() 10 | ).countDocuments({ 11 | 'finality.url': {$exists: true}, 12 | }); 13 | 14 | res.setHeader('Access-Control-Allow-Origin', '*'); 15 | res.setHeader( 16 | 'Access-Control-Allow-Headers', 17 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization' 18 | ); 19 | res.status(200).json({renders}); 20 | return; 21 | } 22 | -------------------------------------------------------------------------------- /pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {redTheme} from '../remotion/theme'; 4 | import {GithubIcon} from '../src/components/Github'; 5 | 6 | const Dashboard = () => { 7 | const [data, setData] = useState({renders: 0}); 8 | 9 | useEffect(() => { 10 | const timeout = setTimeout(() => { 11 | fetch('/api/videos') 12 | .then((res) => res.json()) 13 | .then((res) => { 14 | setData(res); 15 | }); 16 | }, 2000); 17 | 18 | return () => { 19 | clearTimeout(timeout); 20 | }; 21 | }, [data]); 22 | 23 | const percentage = `${(data.renders / 100000) * 100}%`; 24 | 25 | return ( 26 | 37 | 44 |
51 | Road to a 100k rendered videos 52 |
53 |
{data.renders}
54 |
55 | ); 56 | }; 57 | 58 | export default Dashboard; 59 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import {GetServerSideProps} from 'next'; 2 | import React from 'react'; 3 | import {ThemeProvider} from '../remotion/theme'; 4 | import {getCookie} from 'cookies-next'; 5 | import {HomeComponent} from '../src/components/HomeComponent'; 6 | 7 | type Props = { 8 | initialTheme: string | null; 9 | }; 10 | 11 | export const getServerSideProps: GetServerSideProps = async ({ 12 | req, 13 | res, 14 | }) => { 15 | return { 16 | props: { 17 | initialTheme: (getCookie('theme', {req, res}) as string) ?? null, 18 | }, 19 | }; 20 | }; 21 | 22 | export default function Home(props: Props) { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /public/Mona-Sans-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/Mona-Sans-Black.otf -------------------------------------------------------------------------------- /public/Mona-Sans-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/Mona-Sans-Bold.otf -------------------------------------------------------------------------------- /public/Mona-Sans-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/Mona-Sans-ExtraBold.otf -------------------------------------------------------------------------------- /public/Mona-Sans-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/Mona-Sans-Medium.otf -------------------------------------------------------------------------------- /public/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/fav.png -------------------------------------------------------------------------------- /public/favicons/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/favicons/blue.png -------------------------------------------------------------------------------- /public/favicons/golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/favicons/golden.png -------------------------------------------------------------------------------- /public/favicons/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/favicons/green.png -------------------------------------------------------------------------------- /public/favicons/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/favicons/red.png -------------------------------------------------------------------------------- /public/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/flash.png -------------------------------------------------------------------------------- /public/music/track-1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-1.mp3 -------------------------------------------------------------------------------- /public/music/track-1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-1.wav -------------------------------------------------------------------------------- /public/music/track-2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-2.mp3 -------------------------------------------------------------------------------- /public/music/track-2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-2.wav -------------------------------------------------------------------------------- /public/music/track-3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-3.mp3 -------------------------------------------------------------------------------- /public/music/track-3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/music/track-3.wav -------------------------------------------------------------------------------- /public/promo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/promo1.png -------------------------------------------------------------------------------- /public/promo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/promo2.png -------------------------------------------------------------------------------- /public/sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/sound.mp3 -------------------------------------------------------------------------------- /public/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/star.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/watercolour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remotion-dev/github-unwrapped-2022/1297352b814d631fbc9d6338f30cbb55c7d967fd/public/watercolour.png -------------------------------------------------------------------------------- /remotion/AnimatedCommit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {Commit, CommitProps} from './Commit'; 10 | 11 | export const AnimatedCommit: React.FC = ({...props}) => { 12 | const top = interpolate(props.index, [0, 3], [-200, 400]); 13 | const {fps} = useVideoConfig(); 14 | const frame = useCurrentFrame(); 15 | 16 | const offsetProg = spring({ 17 | fps, 18 | frame: frame - 75 - props.index * 2, 19 | config: { 20 | damping: 200, 21 | }, 22 | }); 23 | 24 | const offset = interpolate(offsetProg, [0, 1], [900, 0]); 25 | 26 | return ( 27 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /remotion/AnimatedTopPullRequest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {TopPullRequest, TopPullRequestProps} from "./TopPullRequest"; 10 | 11 | export const AnimatedTopPullRequest: React.FC = ({...props}) => { 12 | const top = interpolate(props.index, [0, 3], [-200, 400]); 13 | const {fps} = useVideoConfig(); 14 | const frame = useCurrentFrame(); 15 | 16 | const offsetProg = spring({ 17 | fps, 18 | frame: frame - 75 - props.index * 2, 19 | config: { 20 | damping: 200, 21 | }, 22 | }); 23 | 24 | const offset = interpolate(offsetProg, [0, 1], [900, 0]); 25 | 26 | return ( 27 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /remotion/Arc.tsx: -------------------------------------------------------------------------------- 1 | import {evolvePath, getLength} from '@remotion/paths'; 2 | import React, {useMemo} from 'react'; 3 | import {AbsoluteFill, Easing, interpolate, useCurrentFrame} from 'remotion'; 4 | import {roughenPath} from './roughen-path'; 5 | 6 | import {SunMoon} from './SunMoon'; 7 | import {Theme} from './theme'; 8 | 9 | export const Arc: React.FC<{ 10 | theme: Theme; 11 | }> = ({theme}) => { 12 | const d = 'M 0 540 C 0 -200 1080 -200 1080 540'; 13 | 14 | const frame = useCurrentFrame(); 15 | 16 | const progress = interpolate(frame, [20, 120], [0.02, 0.99], { 17 | easing: Easing.out(Easing.ease), 18 | extrapolateLeft: 'clamp', 19 | extrapolateRight: 'clamp', 20 | }); 21 | 22 | const pathElement = document.createElementNS( 23 | 'http://www.w3.org/2000/svg', 24 | 'path' 25 | ); 26 | pathElement.setAttribute('d', d); 27 | const x = pathElement.getPointAtLength(progress * getLength(d)).x; 28 | const y = pathElement.getPointAtLength(progress * getLength(d)).y; 29 | 30 | const paths = useMemo(() => { 31 | return roughenPath({ 32 | strokeWidth: 6, 33 | roughness: null, 34 | stroke: 'white', 35 | seed: 2, 36 | bowing: null, 37 | d, 38 | fill: null, 39 | freeze: false, 40 | hachureGap: null, 41 | }); 42 | }, []); 43 | 44 | return ( 45 | 50 | 51 | 57 | {paths.map((p) => { 58 | const {strokeDasharray, strokeDashoffset} = evolvePath( 59 | progress / 2, 60 | p.d 61 | ); 62 | return ( 63 | 72 | ); 73 | })} 74 | 75 | 76 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /remotion/AvgCommits.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion'; 3 | import {Arc} from './Arc'; 4 | import {AvgCommitsTitle, Hour} from './AvgCommitsTitle'; 5 | import {CommitBar} from './CommitBar'; 6 | import {MiddleLine} from './MiddleLine'; 7 | import {Theme} from './theme'; 8 | 9 | export const AvgCommits: React.FC<{ 10 | noBackground: boolean; 11 | theme: Theme; 12 | bestHours: {[key in Hour]: number}; 13 | }> = ({noBackground, theme, bestHours}) => { 14 | const {fps} = useVideoConfig(); 15 | const frame = useCurrentFrame(); 16 | 17 | const values = Object.entries(bestHours); 18 | for (let i = 0; i < 4; i++) { 19 | const hi = values.shift(); 20 | values.push(hi as [string, number]); 21 | } 22 | 23 | const most = Math.max(...values.map((v) => v[1])); 24 | const mostIndex = values.findIndex(([_, b]) => b === most); 25 | const mostHour = values.find(([_, b]) => b === most); 26 | 27 | if (!mostHour) { 28 | throw new Error('No most hour'); 29 | } 30 | 31 | return ( 32 | 37 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 63 | {values.map(([hour, keys], i) => { 64 | return ( 65 | 80 | ); 81 | })} 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /remotion/AvgCommitsTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion'; 3 | 4 | export type Hour = 5 | | 0 6 | | 1 7 | | 2 8 | | 3 9 | | 4 10 | | 5 11 | | 6 12 | | 7 13 | | 8 14 | | 9 15 | | 10 16 | | 11 17 | | 12 18 | | 13 19 | | 14 20 | | 15 21 | | 16 22 | | 17 23 | | 18 24 | | 19 25 | | 20 26 | | 21 27 | | 22 28 | | 23; 29 | 30 | const titles: {[key in Hour]: string} = { 31 | 0: 'My favorite: Commits after midnight.', 32 | 1: 'No late night snacks. Late night commits.', 33 | 2: 'See me commit in the A.M.', 34 | 3: 'See me commit in the A.M.', 35 | 4: 'I love commits before sunrise.', 36 | 5: 'I love commits before sunrise.', 37 | 6: 'Rise, shine, and commit.', 38 | 7: 'Rise, shine, and commit.', 39 | 8: 'I code the most in the early morning.', 40 | 9: 'I code the most in the morning.', 41 | 10: 'I code the most in the morning.', 42 | 11: 'Get those commits in before lunch.', 43 | 12: 'I skip lunch usually.', 44 | 13: "Can't have lunch, need to commit.", 45 | 14: 'I code the most in the afternoon.', 46 | 15: 'I commit the most at 3 PM.', 47 | 16: '4 PM is my favorite time to commit.', 48 | 17: 'Gotta commit before the end of day.', 49 | 18: 'Gotta commit before the end of day.', 50 | 19: 'I code the most in the evening.', 51 | 20: 'I code the most in the evening.', 52 | 21: 'I like to code in the evening.', 53 | 22: 'I make my commits after dark.', 54 | 23: 'Bed time? Nah, Commit time.', 55 | }; 56 | 57 | export const AvgCommitsTitle: React.FC<{topHour: Hour}> = ({topHour}) => { 58 | const frame = useCurrentFrame(); 59 | const title = titles[topHour]; 60 | const words = title.split(' '); 61 | 62 | return ( 63 | 73 |
78 | {words.map((word, i) => { 79 | return ( 80 | 81 | 90 | {word} 91 | 92 | {words.length - 1 !== i ? : null} 93 | 94 | ); 95 | })} 96 |
97 |
98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /remotion/Band.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | 10 | const height = 100; 11 | 12 | export const Band: React.FC<{ 13 | width: number; 14 | height: number; 15 | delay: number; 16 | style: React.CSSProperties; 17 | }> = ({width, delay, style}) => { 18 | const {fps} = useVideoConfig(); 19 | const frame = useCurrentFrame(); 20 | 21 | const spr = spring({ 22 | fps, 23 | frame: frame - delay, 24 | config: { 25 | damping: 200, 26 | }, 27 | durationInFrames: 15, 28 | }); 29 | const left = spr * width; 30 | 31 | const upBowing = 20; 32 | const bowing = interpolate(left, [0, width], [upBowing, 0]); 33 | 34 | const moveIn = interpolate(spr, [0.98, 1], [0, 1], { 35 | extrapolateRight: 'clamp', 36 | extrapolateLeft: 'clamp', 37 | }); 38 | 39 | return ( 40 | 0.98 ? 0 : 3}}> 41 | 48 | 55 | 73 | 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /remotion/BestCommits.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {AnimatedCommit} from './AnimatedCommit'; 10 | import {CompactStats} from './map-response-to-stats'; 11 | import {Theme} from './theme'; 12 | 13 | export const BestCommits: React.FC<{ 14 | stats: CompactStats; 15 | theme: Theme; 16 | noBackground: boolean; 17 | }> = ({stats, theme, noBackground}) => { 18 | const {fps} = useVideoConfig(); 19 | const frame = useCurrentFrame(); 20 | const opacity = interpolate(frame, [30, 60], [0, 1]); 21 | 22 | const moveUp = spring({ 23 | fps, 24 | frame: frame - 75, 25 | config: { 26 | damping: 200, 27 | }, 28 | }); 29 | 30 | return ( 31 | 36 | 42 |

52 | {(stats.pullRequestCount ?? 0) > 0 ? ( 53 | 54 | I crafted {stats.commitCount}{' '} 55 | {stats.commitCount === 1 ? 'commit' : 'commits'} 56 |

and {stats.pullRequestCount}{' '} 57 | {stats.pullRequestCount === 1 ? 'pull request ' : 'pull requests'} 58 | .

59 |
60 | ) : ( 61 | 62 | I crafted {stats.commitCount}{' '} 63 | {stats.commitCount === 1 ? 'commit' : 'commits'}.

64 |
65 | )} 66 | Here are some sweet ones. 67 |

68 |
69 | {stats.bestCommits.map((commit, i) => { 70 | return ( 71 | 72 | 78 | 79 | ); 80 | })} 81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /remotion/Bow.tsx: -------------------------------------------------------------------------------- 1 | import {evolvePath} from '@remotion/paths'; 2 | import React from 'react'; 3 | import {spring, useCurrentFrame, useVideoConfig} from 'remotion'; 4 | 5 | export const Bow: React.FC = () => { 6 | const p1 = 'M490 366.657L946 326.657C961.5 137.157 795 -109.843 490 339.657'; 7 | const p2 = 8 | 'M507.993 366.315L51.9934 326.315C36.4934 136.815 202.993 -110.185 507.993 339.315'; 9 | 10 | const {fps} = useVideoConfig(); 11 | const frame = useCurrentFrame(); 12 | const spr = 13 | 1 - 14 | Math.round( 15 | spring({ 16 | fps, 17 | frame: frame - 30, 18 | config: { 19 | damping: 200, 20 | }, 21 | }) * 50 22 | ) / 23 | 50; 24 | 25 | const progress1 = evolvePath(spr, p1); 26 | const progress2 = evolvePath(spr, p2); 27 | 28 | return ( 29 | 36 | 44 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /remotion/Commit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Bonbon} from './Icons/Bonbon'; 4 | import {Candy} from './Icons/Candy'; 5 | import {Gingerman} from './Icons/Gingerman'; 6 | import {Star} from './Icons/Star'; 7 | import {RoughPath} from './RoughPath'; 8 | import {Theme} from './theme'; 9 | 10 | export type CommitProps = { 11 | message: string; 12 | repository: string; 13 | index: number; 14 | theme: Theme; 15 | }; 16 | 17 | export const commitWidth = 900; 18 | 19 | export const Commit: React.FC = ({ 20 | message, 21 | repository, 22 | index, 23 | theme, 24 | }) => { 25 | return ( 26 | 27 | 33 |
42 |
48 | {index === 0 ? ( 49 | 50 | ) : null} 51 | {index === 1 ? ( 52 | 53 | ) : null} 54 | {index === 2 ? ( 55 | 56 | ) : null} 57 | {index === 3 ? ( 58 | 59 | ) : null} 60 |
61 |
71 |
81 | {repository} 82 |
83 |
94 | {message} 95 |
96 |
97 |
98 |
99 | 105 | 112 | 120 | 121 | 122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /remotion/CommitBar.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import {roughenPath} from './roughen-path'; 3 | import {Theme} from './theme'; 4 | 5 | export const CommitBar: React.FC<{ 6 | height: number; 7 | hour: string; 8 | progress: number; 9 | most: boolean; 10 | theme: Theme; 11 | }> = ({height, hour, progress, most, theme}) => { 12 | const width = 35; 13 | 14 | const actualHeight = progress * height; 15 | 16 | const d = `M 0 0 L 0 ${actualHeight} L ${width} ${actualHeight} L ${width} 0 z`; 17 | 18 | const paths = useMemo(() => { 19 | return roughenPath({ 20 | bowing: null, 21 | d, 22 | fill: most ? theme.mainColor : 'white', 23 | roughness: 0.4, 24 | freeze: false, 25 | hachureGap: null, 26 | seed: Number(hour), 27 | stroke: 'transparent', 28 | strokeWidth: null, 29 | }); 30 | }, [d, hour, most, theme.mainColor]); 31 | 32 | return ( 33 |
38 | 46 | {paths.map((p) => { 47 | return ( 48 | 55 | ); 56 | })} 57 | 58 |
67 | {hour} 68 |
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /remotion/Contribs.tsx: -------------------------------------------------------------------------------- 1 | import {transparentize} from 'polished'; 2 | import {Grill} from '../src/components/Grill'; 3 | import {RoughBox} from '../src/components/RoughBox'; 4 | import {roundSvg} from './round-svg'; 5 | import {useTheme} from './theme'; 6 | 7 | const transparencies = [1, 0, 0.2, 0.8, 0.6, 0.4]; 8 | const padding = 7; 9 | const height = 44; 10 | const width = 66; 11 | 12 | export const Contribs: React.FC<{}> = () => { 13 | const [theme] = useTheme(); 14 | return ( 15 |
22 |
29 | 38 | 39 | 40 |
41 | 42 | 51 |
63 | {new Array(6).fill(true).map((_, i) => { 64 | return ( 65 | 77 | 84 | 85 | ); 86 | })} 87 |
88 |
89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /remotion/EndCard.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {Bauble} from '../src/components/Bauble'; 10 | import {Candy} from './Icons/Candy'; 11 | import {Snow} from './Snow'; 12 | import {Theme} from './theme'; 13 | 14 | export const EndCard: React.FC<{ 15 | noBackground: boolean; 16 | theme: Theme; 17 | }> = ({noBackground, theme}) => { 18 | const frame = useCurrentFrame() - 30; 19 | const {fps, height, width} = useVideoConfig(); 20 | const y = interpolate(frame, [0, 40], [-1000, 600]); 21 | 22 | const rotate = interpolate(frame, [0, 40], [0, -180]) + 170; 23 | 24 | const rotateStuff = spring({ 25 | fps, 26 | frame: frame - 60, 27 | config: { 28 | damping: 200, 29 | }, 30 | durationInFrames: 60, 31 | }); 32 | 33 | const rotation = interpolate(rotateStuff, [0, 1], [140, -140]); 34 | 35 | const secondText = frame < 28; 36 | const endCardStyle: React.CSSProperties = useMemo(() => { 37 | return { 38 | color: secondText ? 'black' : theme.mainColor, 39 | fontFamily: 'MonaSans', 40 | fontSize: 65, 41 | textAlign: 'center', 42 | marginTop: 20, 43 | fontWeight: 900, 44 | }; 45 | }, [secondText, theme.mainColor]); 46 | 47 | return ( 48 | 54 | {noBackground ? null : } 55 | 56 |
69 | 78 |
79 | {secondText 80 | ? `Want to know your own stats?` 81 | : 'Get your #GitHubUnwrapped'} 82 |
83 |
84 |
85 | 96 | 104 |
GitHubUnwrapped.com
105 |
106 |
107 | 112 | 113 | 114 | 115 | 123 | 124 |
125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /remotion/GiftBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | Sequence, 6 | spring, 7 | useCurrentFrame, 8 | useVideoConfig, 9 | } from 'remotion'; 10 | import {AvatarFrame} from './AvatarFrame'; 11 | import {Tree} from './Icons/Tree'; 12 | import {CompactStats} from './map-response-to-stats'; 13 | import {Squeeze} from './Squeeze'; 14 | import {Theme} from './theme'; 15 | import {TitleCard} from './TitleCard'; 16 | import {Unwrap} from './Unwrap'; 17 | import {WallHanger} from './WallHanger'; 18 | 19 | export const GiftBox: React.FC<{userStats: CompactStats; theme: Theme}> = ({ 20 | userStats, 21 | theme, 22 | }) => { 23 | const {fps} = useVideoConfig(); 24 | const frame = useCurrentFrame(); 25 | 26 | const moveAndScaleDown = spring({ 27 | fps, 28 | frame: frame - 64, 29 | config: {}, 30 | }); 31 | const wallHangerComeIn = spring({ 32 | fps, 33 | frame: frame - 70, 34 | config: {damping: 200}, 35 | }); 36 | 37 | const scale = interpolate(moveAndScaleDown, [0, 1], [0, 0.8]); 38 | const translateY = interpolate(moveAndScaleDown, [0, 1], [300, -100]); 39 | 40 | const wallHangerPos = interpolate(wallHangerComeIn, [0, 1], [750, 360]); 41 | const avatarFramePos = interpolate(wallHangerComeIn, [0, 1], [-750, -360]); 42 | 43 | return ( 44 | 45 | 46 | 54 | 55 | 56 | 57 | 65 | 66 | 67 | 75 | 76 | 77 | 78 | 86 | This is my{' '} 87 | 92 | #GitHubUnwrapped 93 | 94 | 95 | } 96 | bigTitle={userStats.username} 97 | theme={theme} 98 | > 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /remotion/GithubComp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Github} from './Github'; 4 | import {Theme} from './theme'; 5 | 6 | export const GithubComp: React.FC<{ 7 | theme: Theme; 8 | }> = ({theme}) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /remotion/GithubPromo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, Internals, Sequence} from 'remotion'; 3 | import {CompProps} from '../src/types'; 4 | import {FeatureList} from './FeatureList'; 5 | import {PromoTitle} from './PromoTitle'; 6 | import {SlideIn, SlideOut, transitionDuration} from './SlideIn'; 7 | import {Snow} from './Snow'; 8 | import {UnwrappedEnd} from './UnwrappedEnd'; 9 | 10 | export const GithubPromo: React.FC = ({stats, type, theme}) => { 11 | const duration = [150, type === 'portrait' ? 220 : 700]; 12 | const accumulatedFrom = (i: number) => 13 | duration.slice(0, i).reduce((a, b) => a + b); 14 | const windPushes = [0, 700] 15 | .map((_, i) => { 16 | if (i === 0) { 17 | return null; 18 | } 19 | return duration.slice(0, i).reduce((a, b) => a + b); 20 | }) 21 | .filter(Internals.truthy); 22 | 23 | return ( 24 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /remotion/IDidALot.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Theme} from './theme'; 4 | 5 | export const IDidALot: React.FC<{ 6 | commitCount: number; 7 | theme: Theme; 8 | }> = ({commitCount, theme}) => { 9 | const text = useMemo(() => { 10 | if (commitCount < 10) { 11 | return '2022 was chill! Just look at my commits:'; 12 | } 13 | if (commitCount < 100) { 14 | return 'I made a few contributions...'; 15 | } 16 | if (commitCount < 1000) { 17 | return 'I made lots of contributions!'; 18 | } 19 | return 'I made tons of contributions!'; 20 | }, [commitCount]); 21 | 22 | const title: React.CSSProperties = useMemo( 23 | () => ({ 24 | color: theme.mainColor, 25 | fontWeight: 'bold', 26 | fontSize: 80, 27 | fontFamily: 'MonaSans', 28 | paddingLeft: 50, 29 | paddingRight: 50, 30 | textAlign: 'center', 31 | }), 32 | [theme.mainColor] 33 | ); 34 | 35 | return ( 36 | 42 |

{text}

43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /remotion/IssueCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps, useMemo} from 'react'; 2 | import {random} from 'remotion'; 3 | import {getRoughGenerator} from './get-rough'; 4 | 5 | export const IssueCircle: React.FC< 6 | SVGProps & { 7 | size: number; 8 | seed: number; 9 | } 10 | > = ({size, seed, ...props}) => { 11 | const paths = useMemo(() => { 12 | const path = getRoughGenerator(); 13 | const drawable = path.circle( 14 | Number(props.cx), 15 | Number(props.cy), 16 | Number(props.r) * 2, 17 | { 18 | roughness: 0.3, 19 | fill: props.fill, 20 | seed: seed, 21 | maxRandomnessOffset: 4, 22 | hachureAngle: random(seed) * 360, 23 | hachureGap: size / 4, 24 | strokeWidth: size / 2, 25 | stroke: props.stroke ?? undefined, 26 | } 27 | ); 28 | 29 | return path.toPaths(drawable); 30 | }, [props.cx, props.cy, props.fill, props.r, props.stroke, seed, size]); 31 | 32 | return ( 33 | <> 34 | {paths.map((p) => { 35 | const {d, stroke, strokeWidth, fill} = p; 36 | return ( 37 | 45 | ); 46 | })} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /remotion/LangPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import {random, useCurrentFrame} from 'remotion'; 2 | import React from 'react'; 3 | import {AbsoluteFill} from 'remotion'; 4 | import {transparentize} from 'polished'; 5 | import {useNoiseTranslate} from './use-noise-translate'; 6 | import {getRoughGenerator} from './get-rough'; 7 | 8 | export const LangPlaceholder: React.FC<{ 9 | name: string; 10 | color: string | null; 11 | }> = ({name, color}) => { 12 | const frame = Math.round(useCurrentFrame() / 4); 13 | const path = getRoughGenerator(); 14 | const drawable = path.circle(50, 50, 100, { 15 | roughness: 1, 16 | fill: transparentize(0.3, color ?? '#ffe577'), 17 | maxRandomnessOffset: 4, 18 | hachureGap: 2, 19 | hachureAngle: random(name) * 360, 20 | strokeWidth: 3, 21 | seed: frame, 22 | stroke: 'black', 23 | }); 24 | 25 | const paths = path.toPaths(drawable); 26 | const [noiseX, noiseY] = useNoiseTranslate(name); 27 | 28 | return ( 29 |
36 | 44 | {paths.map((p) => { 45 | return ( 46 | 56 | ); 57 | })} 58 | 59 | 69 | {name} 70 | 71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /remotion/LanguageToSocks.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AbsoluteFill, 3 | interpolate, 4 | spring, 5 | useCurrentFrame, 6 | useVideoConfig, 7 | } from 'remotion'; 8 | import {TopLanguage} from '../src/get-all'; 9 | import {Socks} from './Socks'; 10 | import {Theme} from './theme'; 11 | import {TopLanguages} from './TopLanguages'; 12 | 13 | export const LanguageToSocks: React.FC<{ 14 | noBackground: boolean; 15 | topLanguages: TopLanguage[]; 16 | theme: Theme; 17 | }> = ({noBackground, topLanguages, theme}) => { 18 | const frame = useCurrentFrame(); 19 | const {fps, width} = useVideoConfig(); 20 | 21 | const spr = spring({ 22 | fps, 23 | frame: frame - 60, 24 | config: { 25 | damping: 200, 26 | }, 27 | }); 28 | 29 | const translateX = interpolate(spr, [0, 1], [0, -width]); 30 | const translateX2 = interpolate(spr, [0, 1], [width, 0]); 31 | 32 | return ( 33 | 34 | 39 | 40 | 41 | 46 | 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /remotion/Languages/CMake.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const CMake: React.FC> = (props) => { 5 | return ( 6 | 7 | 11 | 15 | 19 | 23 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /remotion/Languages/CPlusPlus.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const CPlusPlus: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /remotion/Languages/Clojure.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughEllipse} from '../RoughEllipse'; 3 | 4 | export const Clojure: React.FC> = (props) => { 5 | return ( 6 | 12 | 19 | 23 | 27 | 31 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /remotion/Languages/CoffeeScript.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const CoffeeScript: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 28 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /remotion/Languages/Flutter.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Flutter: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/Languages/GraphQl.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const GraphQL: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /remotion/Languages/HTML.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Html: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/Languages/Haskell.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Haskell: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /remotion/Languages/Java.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Java: React.FC> = (props) => { 5 | return ( 6 | 7 | 11 | 15 | 19 | 23 | 27 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /remotion/Languages/Kotlin.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Kotlin: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/Languages/LanguageIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {LangMapping} from '../language-list'; 3 | 4 | export const LanguageIcon: React.FC<{ 5 | icon: LangMapping; 6 | }> = ({icon}) => { 7 | return ( 8 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /remotion/Languages/Php.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughEllipse} from '../RoughEllipse'; 3 | 4 | export const Php: React.FC> = (props) => { 5 | return ( 6 | 12 | 19 | 23 | 27 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /remotion/Languages/PowerShell.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const PowerShell: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /remotion/Languages/Python.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Python: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /remotion/Languages/RLang.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughEllipse} from '../RoughEllipse'; 3 | 4 | export const RLang: React.FC> = (props) => { 5 | return ( 6 | 12 | 13 | 17 | 21 | 25 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /remotion/Languages/Reason.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | 3 | export const Reason: React.FC> = (props) => { 4 | return ( 5 | 11 | 15 | 19 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /remotion/Languages/Ruby.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Ruby: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /remotion/Languages/Scala.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Scala: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /remotion/Languages/Solidity.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Solidity: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /remotion/Languages/Swift.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Swift: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/Languages/Typescript.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const TypeScript: React.FC> = (props) => { 5 | return ( 6 | 7 | 11 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /remotion/Languages/Vue.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {RoughPath} from '../RoughPath'; 3 | 4 | export const Vue: React.FC> = (props) => { 5 | return ( 6 | 12 | 16 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, Freeze, useCurrentFrame} from 'remotion'; 3 | import {Gift} from './Gift'; 4 | import {Band} from './Band'; 5 | import {Theme} from './theme'; 6 | 7 | export const Loader: React.FC<{ 8 | theme: Theme; 9 | }> = ({theme}) => { 10 | const frame = useCurrentFrame(); 11 | const reverse = 40 - frame; 12 | return ( 13 | 14 | 21 | 22 | 29 | 30 | 31 | 32 | 41 | 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /remotion/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | import {Player} from '@remotion/player'; 2 | import React from 'react'; 3 | import {Loader} from './Loader'; 4 | import {useTheme} from './theme'; 5 | 6 | export const LoadingPage: React.FC = () => { 7 | const [theme] = useTheme(); 8 | return ( 9 |
20 | 36 |
44 | Wrapping... 45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /remotion/MiddleLine.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import {roughenPath} from './roughen-path'; 3 | import {Theme} from './theme'; 4 | 5 | export const MiddleLine: React.FC<{ 6 | theme: Theme; 7 | }> = ({theme}) => { 8 | const d = 'M 0 5 L 1000 5'; 9 | 10 | const paths = useMemo(() => { 11 | return roughenPath({ 12 | strokeWidth: 5, 13 | roughness: 0.9, 14 | stroke: theme.mainColor, 15 | seed: 5, 16 | bowing: null, 17 | d, 18 | fill: null, 19 | freeze: false, 20 | hachureGap: null, 21 | }); 22 | }, [theme.mainColor]); 23 | 24 | return ( 25 | 32 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /remotion/NoIssues.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, useCurrentFrame} from 'remotion'; 3 | import {Gingerman} from './Icons/Gingerman'; 4 | import {RoughEllipse} from './RoughEllipse'; 5 | import {Theme} from './theme'; 6 | 7 | export const NoIssues: React.FC<{ 8 | theme: Theme; 9 | }> = ({theme}) => { 10 | const ry = 200; 11 | const rx = 150; 12 | const frame = useCurrentFrame(); 13 | const y = Math.sin(frame / 10) * ry; 14 | const x = Math.cos(frame / 10) * rx; 15 | 16 | return ( 17 | 23 | 28 | 29 | 30 | 40 | 41 | 42 | 48 | 55 | 56 | 57 | 58 | 70 |
71 | Issues opened this year.

{"I've"} got no complaints! 72 |
73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /remotion/PromoGiftBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | Sequence, 6 | spring, 7 | useCurrentFrame, 8 | useVideoConfig, 9 | } from 'remotion'; 10 | import {CompProps} from '../src/types'; 11 | import {AvatarFrame} from './AvatarFrame'; 12 | import {Tree} from './Icons/Tree'; 13 | import {Squeeze} from './Squeeze'; 14 | import {Theme} from './theme'; 15 | import {TitleCard} from './TitleCard'; 16 | import {Unwrap} from './Unwrap'; 17 | import {WallHanger} from './WallHanger'; 18 | 19 | export const PromoGiftBox: React.FC<{ 20 | theme: Theme; 21 | type: CompProps['type']; 22 | }> = ({theme, type}) => { 23 | const {fps} = useVideoConfig(); 24 | const frame = useCurrentFrame(); 25 | 26 | const moveAndScaleDown = spring({ 27 | fps, 28 | frame: frame - 64, 29 | config: {}, 30 | }); 31 | const wallHangerComeIn = spring({ 32 | fps, 33 | frame: frame - 70, 34 | config: {damping: 200}, 35 | }); 36 | 37 | const scale = interpolate(moveAndScaleDown, [0, 1], [0, 1]); 38 | const translateY = interpolate(moveAndScaleDown, [0, 1], [300, 0]); 39 | 40 | const wallHangerPos = interpolate( 41 | wallHangerComeIn, 42 | [0, 1], 43 | type === 'portrait' ? [900, 370] : [1200, 750] 44 | ); 45 | const avatarFramePos = -interpolate( 46 | wallHangerComeIn, 47 | [0, 1], 48 | type === 'portrait' ? [900, 370] : [1200, 750] 49 | ); 50 | 51 | return ( 52 | 53 | 54 | 62 | 63 | 64 | 65 | 66 | 67 | 74 | Get your personalized 77 | } 78 | theme={theme} 79 | bigTitle={'#GitHubUnwrapped'} 80 | > 81 | 82 | 83 | 91 | 92 | 93 | 101 | 102 | 103 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /remotion/PromoTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {CompProps} from '../src/types'; 4 | import {CompactStats} from './map-response-to-stats'; 5 | import {PromoGiftBox} from './PromoGiftBox'; 6 | import {Theme} from './theme'; 7 | 8 | export const PromoTitle: React.FC<{ 9 | noBackground: boolean; 10 | userStats: CompactStats; 11 | theme: Theme; 12 | type: CompProps['type']; 13 | }> = ({noBackground, theme, type}) => { 14 | return ( 15 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /remotion/Rank.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {RoughCircle} from './RoughCircle'; 3 | 4 | export const Rank: React.FC<{ 5 | num: number; 6 | }> = ({num}) => { 7 | const size = 80; 8 | return ( 9 |
10 | 17 | 25 | 26 |
38 | {num} 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /remotion/RoughCircle.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps, useMemo} from 'react'; 2 | import {useCurrentFrame} from 'remotion'; 3 | import {getRoughGenerator} from './get-rough'; 4 | 5 | import {useNoiseTranslate} from './use-noise-translate'; 6 | 7 | export const RoughCircle: React.FC> = (props) => { 8 | const frame = Math.floor(useCurrentFrame() / 3); 9 | 10 | const [noiseX, noiseY] = useNoiseTranslate(String(props.cx) + props.cy); 11 | 12 | const paths = useMemo(() => { 13 | const path = getRoughGenerator(); 14 | const drawable = path.circle( 15 | Number(props.cx), 16 | Number(props.cy), 17 | Number(props.r) * 2, 18 | { 19 | roughness: 0.3, 20 | fill: props.fill, 21 | seed: frame, 22 | maxRandomnessOffset: 4, 23 | hachureGap: 1, 24 | strokeWidth: (props.strokeWidth as number) ?? 2, 25 | stroke: props.stroke ?? undefined, 26 | } 27 | ); 28 | return path.toPaths(drawable); 29 | }, [ 30 | frame, 31 | props.cx, 32 | props.cy, 33 | props.fill, 34 | props.r, 35 | props.stroke, 36 | props.strokeWidth, 37 | ]); 38 | return ( 39 | <> 40 | {paths.map((p) => { 41 | const {d, stroke, strokeWidth, fill} = p; 42 | return ( 43 | 53 | ); 54 | })} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /remotion/RoughCircleStatic.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps, useMemo} from 'react'; 2 | import {getRoughGenerator} from './get-rough'; 3 | 4 | export const RoughCircleStatic: React.FC< 5 | SVGProps & { 6 | seed: number; 7 | } 8 | > = ({seed, ...props}) => { 9 | const paths = useMemo(() => { 10 | const path = getRoughGenerator(); 11 | const drawable = path.circle( 12 | Number(props.cx), 13 | Number(props.cy), 14 | Number(props.r) * 2, 15 | { 16 | roughness: 0.3, 17 | fill: props.fill, 18 | seed, 19 | maxRandomnessOffset: 4, 20 | hachureGap: 1, 21 | strokeWidth: (props.strokeWidth as number) ?? 2, 22 | stroke: props.stroke ?? undefined, 23 | } 24 | ); 25 | return path.toPaths(drawable); 26 | }, [ 27 | props.cx, 28 | props.cy, 29 | props.fill, 30 | props.r, 31 | props.stroke, 32 | props.strokeWidth, 33 | seed, 34 | ]); 35 | return ( 36 | <> 37 | {paths.map((p) => { 38 | const {d, stroke, strokeWidth, fill} = p; 39 | return ( 40 | 47 | ); 48 | })} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /remotion/RoughEllipse.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {useCurrentFrame} from 'remotion'; 3 | import {getRoughGenerator} from './get-rough'; 4 | import {useNoiseTranslate} from './use-noise-translate'; 5 | 6 | export const RoughEllipse: React.FC< 7 | SVGProps & { 8 | roughness?: number; 9 | } 10 | > = ({roughness, ...props}) => { 11 | const frame = Math.floor(useCurrentFrame() / 3); 12 | const path = getRoughGenerator(); 13 | const drawable = path.ellipse( 14 | Number(props.cx), 15 | Number(props.cy), 16 | Number(props.rx) * 2, 17 | Number(props.ry) * 2, 18 | { 19 | roughness: roughness ?? 0.3, 20 | fill: props.fill, 21 | seed: frame, 22 | maxRandomnessOffset: 4, 23 | hachureGap: 1, 24 | strokeWidth: Number(props.strokeWidth ?? 2), 25 | stroke: props.stroke ?? 'none', 26 | } 27 | ); 28 | 29 | const [noiseX, noiseY] = useNoiseTranslate(String(props.cx) + props.cy); 30 | 31 | const paths = path.toPaths(drawable); 32 | return ( 33 | <> 34 | {paths.map((p) => { 35 | const {d, stroke, strokeWidth, fill} = p; 36 | return ( 37 | 47 | ); 48 | })} 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /remotion/RoughPath.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps, useMemo} from 'react'; 2 | import {useCurrentFrame} from 'remotion'; 3 | import {roughenPath} from './roughen-path'; 4 | 5 | export const RoughPath: React.FC< 6 | SVGProps & { 7 | roughness?: number; 8 | strokeWidth?: number; 9 | hachureGap?: number; 10 | seed?: number; 11 | bowing?: number; 12 | freeze?: boolean; 13 | scaleY?: number; 14 | } 15 | > = ({roughness, scaleY, freeze, strokeWidth, seed, hachureGap, ...props}) => { 16 | const currentFrame = useCurrentFrame(); 17 | const frame = freeze ? 0 : Math.floor(currentFrame / 3); 18 | 19 | const actualSeed = seed ?? frame; 20 | 21 | const paths = useMemo(() => { 22 | return roughenPath({ 23 | bowing: props.bowing ?? null, 24 | d: props.d as string, 25 | fill: props.fill ?? null, 26 | roughness: roughness ?? null, 27 | seed: actualSeed, 28 | freeze: freeze ?? false, 29 | hachureGap: hachureGap ?? null, 30 | stroke: props.stroke ?? null, 31 | strokeWidth: strokeWidth ?? null, 32 | }); 33 | }, [ 34 | actualSeed, 35 | freeze, 36 | hachureGap, 37 | props.bowing, 38 | props.d, 39 | props.fill, 40 | props.stroke, 41 | roughness, 42 | strokeWidth, 43 | ]); 44 | return ( 45 | <> 46 | {paths.map((p) => { 47 | const {d, stroke, strokeWidth, fill} = p; 48 | return ( 49 | 69 | ); 70 | })} 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /remotion/SlideIn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | 10 | export const transitionDuration = 15; 11 | 12 | export const SlideIn: React.FC<{children: React.ReactNode}> = ({children}) => { 13 | const {fps, width} = useVideoConfig(); 14 | const frame = useCurrentFrame(); 15 | 16 | const spr = spring({ 17 | fps, 18 | frame, 19 | config: {damping: 200}, 20 | durationInFrames: transitionDuration, 21 | }); 22 | 23 | return ( 24 | 29 | {children} 30 | 31 | ); 32 | }; 33 | 34 | export const SlideOut: React.FC<{children: React.ReactNode}> = ({children}) => { 35 | const {fps, width, durationInFrames} = useVideoConfig(); 36 | const frame = useCurrentFrame(); 37 | 38 | const spr = spring({ 39 | fps, 40 | frame: frame - (durationInFrames - transitionDuration), 41 | config: {damping: 200}, 42 | durationInFrames: transitionDuration, 43 | }); 44 | 45 | return ( 46 | 51 | {children} 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /remotion/Snow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | random, 6 | spring, 7 | useCurrentFrame, 8 | useVideoConfig, 9 | } from 'remotion'; 10 | 11 | export const Snow: React.FC<{ 12 | windPushes?: number[]; 13 | }> = ({windPushes = [100, 200, 300, 400, 500, 600]}) => { 14 | const {width, height, fps} = useVideoConfig(); 15 | const frame = useCurrentFrame(); 16 | 17 | const wind = windPushes 18 | .map((delay) => { 19 | return ( 20 | spring({ 21 | fps, 22 | frame: frame - delay, 23 | config: { 24 | damping: 200, 25 | }, 26 | durationInFrames: 30, 27 | }) * width 28 | ); 29 | }) 30 | .reduce((a, b) => a + b); 31 | const slidingWindow = Math.max(0, useCurrentFrame() - 150); 32 | 33 | return ( 34 | 39 | {new Array(400).fill(true).map((_, _i) => { 40 | const delay = slidingWindow + _i; 41 | const scale = random(delay + 'size') * 0.5 + 0.5; 42 | const size = scale * 30; 43 | const index = windPushes.findIndex( 44 | (w) => w > delay + interpolate(random(delay), [0, 1], [-75, 75]) 45 | ); 46 | const nextWindPush = 47 | (index === -1 ? windPushes.length - 1 : index - 0.5) + 1; 48 | const pos = random(delay) * (width + size) + nextWindPush * width; 49 | const initialPos = random(delay + 'initial') * height - height / 2; 50 | const speed = (random(delay + 'speed') * height) / 2 + height * 1.5; 51 | 52 | const progress = interpolate(frame - delay, [0, 100], [0, 1]); 53 | const down = interpolate(progress, [0, 1], [0, speed]); 54 | const x = -wind + Math.sin(frame / 20 + delay) * 100; 55 | 56 | return ( 57 |
71 | ); 72 | })} 73 |
74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /remotion/SockComp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | random, 6 | spring, 7 | useCurrentFrame, 8 | useVideoConfig, 9 | } from 'remotion'; 10 | import {Sock} from './Icons/Sock'; 11 | import {TypeScript} from './Languages/Typescript'; 12 | import {Theme} from './theme'; 13 | 14 | export const SockComp: React.FC<{ 15 | children: React.ReactNode; 16 | delay: number; 17 | theme: Theme; 18 | lastLanguage: boolean; 19 | }> = ({children, delay, theme, lastLanguage}) => { 20 | const {fps} = useVideoConfig(); 21 | const frame = useCurrentFrame(); 22 | 23 | const squeezeOut = lastLanguage 24 | ? spring({ 25 | fps, 26 | frame: frame - 40 - delay, 27 | config: { 28 | damping: 200, 29 | }, 30 | durationInFrames: 15, 31 | }) 32 | : 0; 33 | 34 | const squeezeIn = lastLanguage 35 | ? spring({ 36 | fps, 37 | frame: frame - 50 - delay, 38 | config: {}, 39 | durationInFrames: 10, 40 | }) 41 | : 0; 42 | const push = spring({ 43 | fps, 44 | frame: frame - (lastLanguage ? 50 : 10) - delay, 45 | durationInFrames: 20, 46 | }); 47 | 48 | const sockRotation = spring({ 49 | fps, 50 | frame: frame - delay, 51 | durationInFrames: 20, 52 | }); 53 | 54 | const scaleX = 1 + squeezeOut * 0.2 - squeezeIn * 0.2; 55 | const scaleY = 1 - squeezeOut * 0.2 + squeezeIn * 0.2; 56 | 57 | const top = interpolate(push, [0, 1], [0, -350]); 58 | const scaleLogo = interpolate(push, [0, 1], [0.15, 0.3]); 59 | 60 | const comp = children ?? ; 61 | 62 | return ( 63 | 79 | 86 | {comp} 87 | 88 | 94 | 101 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /remotion/Socks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion'; 3 | import {TopLanguage} from '../src/get-all'; 4 | import {SockComp} from './SockComp'; 5 | import {Theme} from './theme'; 6 | import {TopLanguagePodium} from './top-language-stairs'; 7 | import {Lang} from './TopLang'; 8 | 9 | export const Socks: React.FC<{ 10 | noBackground: boolean; 11 | topLanguages: TopLanguage[]; 12 | theme: Theme; 13 | delay?: number; 14 | }> = ({noBackground, topLanguages, theme, delay = 0}) => { 15 | const {width, fps} = useVideoConfig(); 16 | const top3Languages = topLanguages.slice(0, 3).reverse(); 17 | const frame = useCurrentFrame(); 18 | const offset = 19 | top3Languages.length === 1 20 | ? 0 21 | : new Array(top3Languages.length - 1) 22 | .fill(true) 23 | .map((_, i) => { 24 | return spring({ 25 | fps, 26 | frame: 27 | frame - 28 | (i + 1) * (i === top3Languages.length - 2 ? 40 : 40) - 29 | delay, 30 | config: { 31 | damping: 200, 32 | }, 33 | }); 34 | }) 35 | .reduce((a, b) => a + b); 36 | 37 | return ( 38 | 43 | 48 | 54 | {top3Languages.map((language, i) => { 55 | const lastLanguage = i === top3Languages.length - 1; 56 | return ( 57 | 63 | 68 | 69 | 70 | 71 | ); 72 | })} 73 | 74 | 75 | 85 | 89 | 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /remotion/Squeeze.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion'; 3 | 4 | export const Squeeze: React.FC<{ 5 | children: React.ReactNode; 6 | delay: number; 7 | direction: 'horizontal' | 'vertical'; 8 | }> = ({children, delay, direction}) => { 9 | const frame = useCurrentFrame(); 10 | const {fps} = useVideoConfig(); 11 | const squeezeIn = spring({ 12 | fps, 13 | frame: frame - 10 - delay, 14 | durationInFrames: 10, 15 | }); 16 | 17 | const squeezeOut = spring({ 18 | fps, 19 | frame: frame - delay, 20 | config: { 21 | damping: 200, 22 | }, 23 | durationInFrames: 15, 24 | }); 25 | 26 | const scaleX = 1 + squeezeOut * 0.1 - squeezeIn * 0.1; 27 | const scaleY = 1 - squeezeOut * 0.1 + squeezeIn * 0.1; 28 | 29 | const transform = 30 | direction === 'horizontal' 31 | ? `scaleX(${scaleX}) scaleY(${scaleY})` 32 | : `scaleX(${scaleY}) scaleY(${scaleX})`; 33 | 34 | return ( 35 | 40 | {children} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /remotion/SunMoon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, interpolate} from 'remotion'; 3 | import {Sun} from '../src/components/Sun'; 4 | import {iosSafariOrFirefox} from './ios-safari'; 5 | import {Moon} from './Moon'; 6 | import {Theme} from './theme'; 7 | 8 | export const SunMoon: React.FC<{ 9 | progress: number; 10 | theme: Theme; 11 | }> = ({progress, theme}) => { 12 | const rotation = interpolate( 13 | progress, 14 | [0.2, 0.3, 0.7, 0.8], 15 | [0, Math.PI, Math.PI, 0], 16 | { 17 | extrapolateLeft: 'clamp', 18 | extrapolateRight: 'clamp', 19 | } 20 | ); 21 | 22 | const rotationPrimitive = progress > 0.25 && progress < 0.75; 23 | 24 | return ( 25 | 31 |
44 |
67 | 74 |
75 |
98 | 105 |
106 |
107 |
108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /remotion/Teaser.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, Internals, Sequence} from 'remotion'; 3 | import {CompProps} from '../src/types'; 4 | import {transitionDuration} from './SlideIn'; 5 | import {Snow} from './Snow'; 6 | import {TeaserGift} from './TeaserGift'; 7 | 8 | export const Teaser: React.FC = ({type, theme}) => { 9 | const duration = [400]; 10 | const windPushes = [0, 700] 11 | .map((_, i) => { 12 | if (i === 0) { 13 | return null; 14 | } 15 | return duration.slice(0, i).reduce((a, b) => a + b); 16 | }) 17 | .filter(Internals.truthy); 18 | 19 | return ( 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /remotion/Title2022.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {GiftBox} from './GiftBox'; 4 | import {CompactStats} from './map-response-to-stats'; 5 | import {Theme} from './theme'; 6 | 7 | export const Title: React.FC<{ 8 | noBackground: boolean; 9 | userStats: CompactStats; 10 | theme: Theme; 11 | }> = ({noBackground, userStats, theme}) => { 12 | return ( 13 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /remotion/TitleCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {RoughPath} from './RoughPath'; 10 | import {Theme} from './theme'; 11 | 12 | const titleStyle: React.CSSProperties = { 13 | color: 'black', 14 | fontFamily: 'MonaSans', 15 | fontSize: 80, 16 | textAlign: 'center', 17 | fontWeight: 'bold', 18 | lineHeight: 1.1, 19 | }; 20 | 21 | export const TitleCard: React.FC<{ 22 | theme: Theme; 23 | smallTitle: React.ReactNode; 24 | bigTitle: React.ReactNode; 25 | }> = ({theme, smallTitle, bigTitle}) => { 26 | const {fps} = useVideoConfig(); 27 | const frame = useCurrentFrame(); 28 | 29 | const spr = spring({ 30 | fps, 31 | frame, 32 | config: { 33 | damping: 200, 34 | }, 35 | }); 36 | 37 | const translateY = interpolate(spr, [0, 1], [800, 300]); 38 | 39 | return ( 40 | 46 | 52 | 59 | 68 | 69 | {' '} 70 | 76 |
77 | {smallTitle} 78 |
79 | 86 | {bigTitle} 87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /remotion/TopLang.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {TopLanguage} from '../src/get-all'; 4 | import {LangPlaceholder} from './LangPlaceholder'; 5 | import {languageList} from './language-list'; 6 | import {LanguageIcon} from './Languages/LanguageIcon'; 7 | import {Theme} from './theme'; 8 | import {TopLangTitle} from './TopLangTitle'; 9 | 10 | export const TopLang: React.FC<{ 11 | topLanguages: TopLanguage[]; 12 | theme: Theme; 13 | }> = ({topLanguages, theme}) => { 14 | return ( 15 | 21 | 22 | 32 | {topLanguages.map((l) => { 33 | return ; 34 | })} 35 | 36 | 37 | ); 38 | }; 39 | 40 | const style: React.CSSProperties = { 41 | display: 'inline-block', 42 | }; 43 | 44 | export const Lang: React.FC<{ 45 | lang: TopLanguage; 46 | }> = ({lang}) => { 47 | const icon = languageList.find((f) => { 48 | return f.name === lang.name; 49 | }); 50 | 51 | return ( 52 |
53 | 63 | {icon ? ( 64 | 65 | ) : ( 66 | 70 | )} 71 | 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /remotion/TopLangTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, interpolate, useCurrentFrame} from 'remotion'; 3 | import {Theme} from './theme'; 4 | 5 | export const TopLangTitle: React.FC<{ 6 | theme: Theme; 7 | }> = ({theme}) => { 8 | const frame = useCurrentFrame(); 9 | const title = `I speak many languages...`; 10 | const words = title.split(' '); 11 | 12 | return ( 13 | 23 |
28 | {words.map((word, i) => { 29 | return ( 30 | <> 31 | 40 | {word} 41 | 42 | {words.length - 1 !== i ? : null} 43 | 44 | ); 45 | })} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /remotion/TopLanguageIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {spring, useCurrentFrame, useVideoConfig} from 'remotion'; 3 | import {TopLanguage} from '../src/get-all'; 4 | import {Rank} from './Rank'; 5 | 6 | const row: React.CSSProperties = { 7 | display: 'flex', 8 | flexDirection: 'row', 9 | alignItems: 'center', 10 | marginTop: 10, 11 | marginBottom: 10, 12 | justifyContent: 'center', 13 | lineHeight: 1, 14 | }; 15 | 16 | export const TopLanguageIcon: React.FC<{ 17 | reverseIndex: number; 18 | delay: number; 19 | language: TopLanguage; 20 | num: number; 21 | top: boolean; 22 | }> = ({reverseIndex, top, num, delay, language}) => { 23 | const {fps} = useVideoConfig(); 24 | const frame = useCurrentFrame(); 25 | 26 | const opacity = spring({ 27 | fps, 28 | frame: frame - (reverseIndex + 1) * 40 + 30 - delay - (num === 1 ? 40 : 0), 29 | config: { 30 | damping: 200, 31 | }, 32 | }); 33 | 34 | return ( 35 |
43 |
44 | 45 |
46 |
{language.name}
47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /remotion/TopLanguages.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {TopLanguage} from '../src/get-all'; 4 | 5 | export const TopLanguages: React.FC<{ 6 | languages: TopLanguage[]; 7 | }> = ({languages}) => { 8 | return ( 9 | 20 | My top {languages.length === 1 ? 'language' : 'languages'} of 2022 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /remotion/TopPullRequest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {RoughPath} from './RoughPath'; 4 | 5 | export type TopPullRequestProps = { 6 | organization: string; 7 | repository: string; 8 | pullRequestsCount: number; 9 | index: number; 10 | }; 11 | 12 | export const commitWidth = 900; 13 | 14 | export const TopPullRequest: React.FC = ({ 15 | organization, 16 | repository, 17 | pullRequestsCount, 18 | index 19 | }) => { 20 | return ( 21 | 22 | 28 |
37 |
50 | {pullRequestsCount} × 51 |
52 |
62 |
72 | {organization} 73 |
74 |
85 | {repository} 86 |
87 |
88 |
89 |
90 | 96 | 103 | 111 | 112 | 113 |
114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /remotion/TotalContributions.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | spring, 6 | useCurrentFrame, 7 | useVideoConfig, 8 | } from 'remotion'; 9 | import {Theme} from './theme'; 10 | 11 | export const TotalContributions: React.FC<{ 12 | totalContributions: number; 13 | theme: Theme; 14 | }> = ({totalContributions, theme}) => { 15 | const {fps} = useVideoConfig(); 16 | const frame = useCurrentFrame(); 17 | 18 | const title: React.CSSProperties = useMemo( 19 | () => ({ 20 | textAlign: 'center', 21 | fontSize: 200, 22 | fontFamily: 'MonaSans', 23 | color: theme.mainColor, 24 | fontWeight: 'bold', 25 | }), 26 | [theme.mainColor] 27 | ); 28 | 29 | const subtitle: React.CSSProperties = useMemo( 30 | () => ({ 31 | textAlign: 'center', 32 | fontSize: 36, 33 | fontFamily: 'MonaSans', 34 | color: theme.mainColor, 35 | fontWeight: 'bold', 36 | }), 37 | [theme.mainColor] 38 | ); 39 | 40 | const prog = spring({ 41 | fps, 42 | frame, 43 | config: { 44 | damping: 200, 45 | }, 46 | }); 47 | 48 | const num = interpolate(prog, [0, 0.9], [0, totalContributions], { 49 | extrapolateRight: 'clamp', 50 | }); 51 | const scale = interpolate(prog, [0, 1], [0.6, 1.2]); 52 | 53 | const op = interpolate(prog, [0.9, 1], [0, 1]); 54 | 55 | return ( 56 | 63 |
{Math.round(num)}
64 |
to be exact!
65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /remotion/TreeComp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Tree} from './Icons/Tree'; 4 | import {Theme} from './theme'; 5 | 6 | export const TreeComp: React.FC<{theme: Theme}> = ({theme}) => { 7 | return ( 8 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /remotion/TreeGithub.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Github} from './Github'; 4 | import {Tree} from './Icons/Tree'; 5 | import {Theme} from './theme'; 6 | 7 | export const TreeGithub: React.FC<{ 8 | theme: Theme; 9 | }> = ({theme}) => { 10 | return ( 11 | 12 | 18 | 19 | 20 | 26 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /remotion/Unwrap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Gift} from './Gift'; 4 | import {Band} from './Band'; 5 | import {Theme} from './theme'; 6 | 7 | export const Unwrap: React.FC<{ 8 | theme: Theme; 9 | }> = ({theme}) => { 10 | const delay = 30; 11 | return ( 12 | 13 | 20 | 27 | 28 | 37 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /remotion/UnwrappedEnd.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {GithubIcon} from '../src/components/Github'; 4 | import {redTheme} from './theme'; 5 | 6 | export const UnwrappedEnd: React.FC = () => { 7 | return ( 8 | 19 | 25 | GitHubUnwrapped.com 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /remotion/WeekdayBar.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import {spring, useCurrentFrame, useVideoConfig} from 'remotion'; 3 | import {parsePath, roundCommands} from 'svg-round-corners'; 4 | import {RoughPath} from './RoughPath'; 5 | import {Theme} from './theme'; 6 | 7 | export const WeekdayBar: React.FC<{ 8 | height: number; 9 | isMostProductive: boolean; 10 | index: number; 11 | theme: Theme; 12 | }> = ({height, isMostProductive, index, theme}) => { 13 | const frame = useCurrentFrame(); 14 | 15 | const {fps} = useVideoConfig(); 16 | const progress = spring({ 17 | fps, 18 | frame: frame - index * 2, 19 | config: { 20 | damping: 200, 21 | }, 22 | }); 23 | 24 | const width = 90; 25 | 26 | const d = useMemo(() => { 27 | const parsed = parsePath( 28 | `M 0 0 L 0 ${height} L ${width} ${height} L ${width} 0 z` 29 | ); 30 | 31 | return roundCommands(parsed, 15).path; 32 | }, [height]); 33 | 34 | return ( 35 | <> 36 | 44 | {isMostProductive ? ( 45 | 54 | ) : ( 55 | 67 | )} 68 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /remotion/font.ts: -------------------------------------------------------------------------------- 1 | import {continueRender, delayRender, staticFile} from 'remotion'; 2 | 3 | if (typeof window !== 'undefined' && 'FontFace' in window) { 4 | const font = new FontFace( 5 | 'MonaSans', 6 | 'url(' + staticFile('Mona-Sans-Medium.otf') + ')', 7 | {weight: '500'} 8 | ); 9 | const handle = delayRender(); 10 | font.load().then(() => { 11 | document.fonts.add(font); 12 | continueRender(handle); 13 | }); 14 | const font2 = new FontFace( 15 | 'MonaSans', 16 | 'url(' + staticFile('Mona-Sans-Bold.otf') + ')', 17 | { 18 | weight: '700', 19 | } 20 | ); 21 | const handle2 = delayRender(); 22 | font2.load().then(() => { 23 | document.fonts.add(font2); 24 | continueRender(handle2); 25 | }); 26 | const font3 = new FontFace( 27 | 'MonaSans', 28 | 'url(' + staticFile('Mona-Sans-ExtraBold.otf') + ')', 29 | { 30 | weight: '900', 31 | } 32 | ); 33 | const handle3 = delayRender(); 34 | font3.load().then(() => { 35 | document.fonts.add(font3); 36 | continueRender(handle3); 37 | }); 38 | } 39 | 40 | export const getFont = () => null; 41 | -------------------------------------------------------------------------------- /remotion/frontend-stats.ts: -------------------------------------------------------------------------------- 1 | export type Commit = { 2 | message: string; 3 | author: string; 4 | repo: string; 5 | date: number; 6 | }; 7 | 8 | export type PullRequest = { 9 | uniqueId: string; 10 | title: string; 11 | organization: string; 12 | repository: string; 13 | }; 14 | 15 | export type Weekday = '0' | '1' | '2' | '3' | '4' | '5' | '6'; 16 | 17 | export type Weekdays = { 18 | least: Weekday; 19 | leastCount: number; 20 | mostCount: number; 21 | most: Weekday; 22 | ratio: number; 23 | days: number[]; 24 | }; 25 | 26 | export type FrontendStats = Commit[]; 27 | -------------------------------------------------------------------------------- /remotion/get-rough.ts: -------------------------------------------------------------------------------- 1 | import type {RoughGenerator} from 'roughjs/bin/generator'; 2 | // @ts-expect-error 3 | import rough from 'roughjs/bundled/rough.cjs'; 4 | 5 | export const getRough = () => { 6 | return rough as typeof import('roughjs').default; 7 | }; 8 | 9 | let generator: RoughGenerator | null = null; 10 | 11 | export const getRoughGenerator = () => { 12 | const r = getRough(); 13 | if (!generator) { 14 | generator = r.generator(); 15 | } 16 | 17 | return generator; 18 | }; 19 | -------------------------------------------------------------------------------- /remotion/github-api.ts: -------------------------------------------------------------------------------- 1 | import type {commits} from './commits'; 2 | import {Commit} from './frontend-stats'; 3 | import {mapApiResponseToCommits} from './map-api-response-to-commits'; 4 | 5 | export const RATE_LIMIT_TOKEN = 'rate-limit-token'; 6 | 7 | export const getGithubCommits = async (username: string, page: number) => { 8 | const response = await fetch( 9 | `https://api.github.com/search/commits?q=author:${username}%20merge:false&sort=author-date&order=desc&page=${page}&per_page=100` 10 | ); 11 | if (response.status !== 200) { 12 | // TODO: Distinguish between 404 and rate limit 13 | const message = (await response.json()).message; 14 | if (message.includes('API rate limit')) { 15 | throw new TypeError(RATE_LIMIT_TOKEN); 16 | } 17 | throw new TypeError((await response.json()).message); 18 | } 19 | const json = (await response.json()) as typeof commits; 20 | const listOfCommits = mapApiResponseToCommits(json); 21 | const isDone = 22 | listOfCommits.length === 0 || 23 | listOfCommits[listOfCommits.length - 1].date < 24 | new Date('2022-01-01').getTime(); 25 | return { 26 | commits: listOfCommits, 27 | isDone, 28 | }; 29 | }; 30 | 31 | export const getALotOfGithubCommits = async (username: string) => { 32 | let listOfCommits: Commit[] = []; 33 | 34 | let pages = [1, 2, 3, 4, 5]; 35 | 36 | for (const page of pages) { 37 | const {commits, isDone} = await getGithubCommits(username, page); 38 | listOfCommits.push(...commits); 39 | if (isDone) { 40 | break; 41 | } 42 | } 43 | 44 | return listOfCommits; 45 | }; 46 | -------------------------------------------------------------------------------- /remotion/index.ts: -------------------------------------------------------------------------------- 1 | import {registerRoot} from 'remotion'; 2 | import {getFont} from './font'; 3 | import {Root} from './Root'; 4 | 5 | // FIXME: Vitest does not recognize it as a function 6 | if (typeof registerRoot === 'function') { 7 | registerRoot(Root); 8 | } 9 | 10 | getFont(); 11 | -------------------------------------------------------------------------------- /remotion/ios-safari.ts: -------------------------------------------------------------------------------- 1 | export const iosSafariOrFirefox = () => { 2 | if (typeof window === 'undefined') { 3 | return false; 4 | } 5 | const ua = window.navigator.userAgent; 6 | const iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i); 7 | const webkit = !!ua.match(/WebKit/i); 8 | return (iOS && webkit) || ua.match(/Firefox/i); 9 | }; 10 | -------------------------------------------------------------------------------- /remotion/language-list.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps} from 'react'; 2 | import {Clojure} from './Languages/Clojure'; 3 | import {CMake} from './Languages/CMake'; 4 | import {CoffeeScript} from './Languages/CoffeeScript'; 5 | import {CPlusPlus} from './Languages/CPlusPlus'; 6 | import {Css} from './Languages/Css'; 7 | import {Flutter} from './Languages/Flutter'; 8 | import {GraphQL} from './Languages/GraphQl'; 9 | import {Haskell} from './Languages/Haskell'; 10 | import {Html} from './Languages/HTML'; 11 | import {Java} from './Languages/Java'; 12 | import {JavaScript} from './Languages/JavaScript'; 13 | import {Kotlin} from './Languages/Kotlin'; 14 | import {Lua} from './Languages/Lua'; 15 | import {Php} from './Languages/Php'; 16 | import {PowerShell} from './Languages/PowerShell'; 17 | import {Python} from './Languages/Python'; 18 | import {Reason} from './Languages/Reason'; 19 | import {RLang} from './Languages/RLang'; 20 | import {Ruby} from './Languages/Ruby'; 21 | import {Rust} from './Languages/Rust'; 22 | import {Sass} from './Languages/Sass'; 23 | import {Scala} from './Languages/Scala'; 24 | import {Solidity} from './Languages/Solidity'; 25 | import {SQL} from './Languages/SQL'; 26 | import {Swift} from './Languages/Swift'; 27 | import {TypeScript} from './Languages/Typescript'; 28 | import {Vue} from './Languages/Vue'; 29 | 30 | export type LangMapping = { 31 | name: string; 32 | Component: React.FC>; 33 | }; 34 | 35 | export const NotLanguages = ['Markdown', 'Dockerfile', 'Roff', 'Shell']; 36 | 37 | export const languageList: LangMapping[] = [ 38 | { 39 | name: 'Clojure', 40 | Component: Clojure, 41 | }, 42 | { 43 | name: 'CMake', 44 | Component: CMake, 45 | }, 46 | { 47 | name: 'CoffeeScript', 48 | Component: CoffeeScript, 49 | }, 50 | { 51 | name: 'C++', 52 | Component: CPlusPlus, 53 | }, 54 | { 55 | name: 'CSS', 56 | Component: Css, 57 | }, 58 | { 59 | name: 'Solidity', 60 | Component: Solidity, 61 | }, 62 | { 63 | name: 'Dart', 64 | Component: Flutter, 65 | }, 66 | { 67 | name: 'GraphQL', 68 | Component: GraphQL, 69 | }, 70 | { 71 | name: 'Haskell', 72 | Component: Haskell, 73 | }, 74 | { 75 | name: 'HTML', 76 | Component: Html, 77 | }, 78 | { 79 | name: 'Java', 80 | Component: Java, 81 | }, 82 | { 83 | name: 'JavaScript', 84 | Component: JavaScript, 85 | }, 86 | { 87 | name: 'Kotlin', 88 | Component: Kotlin, 89 | }, 90 | { 91 | name: 'Lua', 92 | Component: Lua, 93 | }, 94 | { 95 | name: 'SQL', 96 | Component: SQL, 97 | }, 98 | { 99 | name: 'PHP', 100 | Component: Php, 101 | }, 102 | { 103 | name: 'PowerShell', 104 | Component: PowerShell, 105 | }, 106 | { 107 | name: 'Python', 108 | Component: Python, 109 | }, 110 | { 111 | name: 'R', 112 | Component: RLang, 113 | }, 114 | { 115 | name: 'Reason', 116 | Component: Reason, 117 | }, 118 | { 119 | name: 'Ruby', 120 | Component: Ruby, 121 | }, 122 | { 123 | name: 'Rust', 124 | Component: Rust, 125 | }, 126 | { 127 | name: 'Sass', 128 | Component: Sass, 129 | }, 130 | { 131 | name: 'Scala', 132 | Component: Scala, 133 | }, 134 | { 135 | name: 'Swift', 136 | Component: Swift, 137 | }, 138 | { 139 | name: 'TypeScript', 140 | Component: TypeScript, 141 | }, 142 | { 143 | name: 'Vue', 144 | Component: Vue, 145 | }, 146 | ]; 147 | -------------------------------------------------------------------------------- /remotion/map-api-response-to-commits.ts: -------------------------------------------------------------------------------- 1 | import {commits} from './commits'; 2 | import {Commit} from './frontend-stats'; 3 | 4 | type CommitsApiResponse = typeof commits; 5 | 6 | export const mapApiResponseToCommits = ( 7 | commitApiResponse: CommitsApiResponse 8 | ): Commit[] => { 9 | return commitApiResponse.items 10 | .map((commit) => { 11 | if (!commit.author) { 12 | return null; 13 | } 14 | return { 15 | author: commit.author.login, 16 | date: new Date(commit.commit.author.date).getTime(), 17 | message: commit.commit.message, 18 | repo: commit.repository.owner.login + '/' + commit.repository.name, 19 | }; 20 | }) 21 | .filter(Boolean) as Commit[]; 22 | }; 23 | -------------------------------------------------------------------------------- /remotion/og/Og.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill, Sequence} from 'remotion'; 3 | import {StillTitleCard} from './StillTitleCard'; 4 | import {StillWallHanger} from './StillWallHanger'; 5 | import {StillAvatarFrame} from './StillAvatarFrame'; 6 | import {StaticSnow} from './StaticSnow'; 7 | import {StaticTree} from './StaticTree'; 8 | import {OgCompProps} from '../../src/types'; 9 | 10 | export const OG: React.FC = ({userStats, theme, isGeneric}) => { 11 | const wallHangerPos = 360; 12 | const avatarFramePos = -360; 13 | 14 | return ( 15 |
16 | 17 | 24 | 28 | 29 | 36 | 39 | 40 | 47 | 52 | 53 | 54 | 59 | 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /remotion/og/StaticRoughPath.tsx: -------------------------------------------------------------------------------- 1 | import React, {SVGProps, useMemo} from 'react'; 2 | import {random} from 'remotion'; 3 | import {getRoughGenerator} from '../get-rough'; 4 | 5 | export const StaticRoughPath: React.FC< 6 | SVGProps & { 7 | roughness?: number; 8 | strokeWidth?: number; 9 | hachureGap?: number; 10 | seed?: number; 11 | bowing?: number; 12 | freeze?: boolean; 13 | } 14 | > = ({roughness, freeze, strokeWidth, seed, hachureGap, ...props}) => { 15 | const currentFrame = 50; 16 | const frame = freeze ? 0 : Math.floor(currentFrame / 3); 17 | 18 | const paths = useMemo(() => { 19 | const path = getRoughGenerator(); 20 | const drawable = path.path(props.d as string, { 21 | roughness: roughness ?? 0.3, 22 | fill: props.fill, 23 | seed: seed ?? frame, 24 | maxRandomnessOffset: 4, 25 | hachureGap: hachureGap ?? 1, 26 | hachureAngle: freeze 27 | ? random(seed ?? '') * 360 28 | : random(seed ?? props.d ?? '') * 360, 29 | strokeWidth: strokeWidth ?? 2, 30 | stroke: props.stroke ?? undefined, 31 | bowing: props.bowing ?? 1, 32 | }); 33 | 34 | return path.toPaths(drawable); 35 | }, [ 36 | frame, 37 | freeze, 38 | hachureGap, 39 | props.bowing, 40 | props.d, 41 | props.fill, 42 | props.stroke, 43 | roughness, 44 | seed, 45 | strokeWidth, 46 | ]); 47 | return ( 48 | <> 49 | {paths.map((p) => { 50 | const {d, stroke, strokeWidth, fill} = p; 51 | return ( 52 | 59 | ); 60 | })} 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /remotion/og/StaticSnow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AbsoluteFill, 4 | interpolate, 5 | random, 6 | spring, 7 | useCurrentFrame, 8 | useVideoConfig, 9 | } from 'remotion'; 10 | 11 | export const StaticSnow: React.FC<{ 12 | windPushes?: number[]; 13 | }> = ({windPushes = [100, 200, 300, 400, 500, 600]}) => { 14 | const {width, height, fps} = useVideoConfig(); 15 | const frame = 50; 16 | 17 | const wind = windPushes 18 | .map((delay) => { 19 | return ( 20 | spring({ 21 | fps, 22 | frame: frame - delay, 23 | config: { 24 | damping: 200, 25 | }, 26 | durationInFrames: 30, 27 | }) * width 28 | ); 29 | }) 30 | .reduce((a, b) => a + b); 31 | const slidingWindow = Math.max(0, useCurrentFrame() - 150); 32 | 33 | return ( 34 | 39 | {new Array(400).fill(true).map((_, _i) => { 40 | const delay = slidingWindow + _i; 41 | const scale = random(delay + 'size') * 0.5 + 0.5; 42 | const size = scale * 30; 43 | const index = windPushes.findIndex( 44 | (w) => w > delay + interpolate(random(delay), [0, 1], [-75, 75]) 45 | ); 46 | const nextWindPush = 47 | (index === -1 ? windPushes.length - 1 : index - 0.5) + 1; 48 | const pos = random(delay) * (width + size) + nextWindPush * width; 49 | const initialPos = random(delay + 'initial') * height - height / 2; 50 | const speed = (random(delay + 'speed') * height) / 2 + height * 1.5; 51 | 52 | const progress = interpolate(frame - delay, [0, 100], [0, 1]); 53 | const down = interpolate(progress, [0, 1], [0, speed]); 54 | const x = 55 | interpolate(progress, [0, 1], [-wind, -wind]) + 56 | Math.sin(frame / 20 + delay) * 100; 57 | 58 | return ( 59 |
73 | ); 74 | })} 75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /remotion/og/StillTitleCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AbsoluteFill} from 'remotion'; 3 | import {Theme} from '../theme'; 4 | import {StaticRoughPath} from './StaticRoughPath'; 5 | 6 | const titleStyle: React.CSSProperties = { 7 | color: 'black', 8 | fontFamily: 'MonaSans', 9 | fontSize: 80, 10 | textAlign: 'center', 11 | fontWeight: 'bold', 12 | lineHeight: 1.1, 13 | }; 14 | 15 | export const StillTitleCard: React.FC<{ 16 | username: string; 17 | theme: Theme; 18 | isGeneric: boolean; 19 | }> = ({username, theme, isGeneric}) => { 20 | return ( 21 | 27 | 33 | 40 | 48 | 49 | {' '} 50 | 56 | {!isGeneric ? ( 57 |
58 | 64 | 69 | {username} 70 | {"'s"} 71 | 72 | 73 |
74 | 81 | #GitHubUnwrapped 82 | 83 |
84 | ) : ( 85 |
86 | 92 | 97 | Your 2022 in review 98 | 99 | 100 |
101 | 108 | #GitHubUnwrapped 109 | 110 |
111 | )} 112 |
113 |
114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /remotion/roughen-path.ts: -------------------------------------------------------------------------------- 1 | import {random} from 'remotion'; 2 | import {PathInfo} from 'roughjs/bin/core'; 3 | import {getRoughGenerator} from './get-rough'; 4 | 5 | const cache: Record = {}; 6 | 7 | export const roughenPath = ({ 8 | d, 9 | fill, 10 | roughness, 11 | seed, 12 | hachureGap, 13 | freeze, 14 | strokeWidth, 15 | stroke, 16 | bowing, 17 | }: { 18 | d: string; 19 | roughness: number | null; 20 | fill: string | null; 21 | seed: number; 22 | hachureGap: number | null; 23 | freeze: boolean; 24 | strokeWidth: number | null; 25 | stroke: string | null; 26 | bowing: number | null; 27 | }) => { 28 | const key = [ 29 | d, 30 | roughness, 31 | fill, 32 | seed, 33 | hachureGap, 34 | freeze, 35 | strokeWidth, 36 | stroke, 37 | bowing, 38 | ].join('-'); 39 | 40 | if (cache[key]) { 41 | return cache[key]; 42 | } 43 | 44 | const path = getRoughGenerator(); 45 | const drawable = path.path(d, { 46 | roughness: roughness ?? 0.3, 47 | fill: fill ?? undefined, 48 | seed: seed, 49 | maxRandomnessOffset: 4, 50 | hachureGap: hachureGap ?? 1, 51 | hachureAngle: freeze 52 | ? random(seed ?? '') * 360 53 | : random(seed ?? d ?? '') * 360, 54 | strokeWidth: strokeWidth ?? 2, 55 | stroke: stroke ?? undefined, 56 | bowing: bowing ?? 1, 57 | }); 58 | 59 | const paths = path.toPaths(drawable); 60 | cache[key] = paths; 61 | 62 | return paths; 63 | }; 64 | -------------------------------------------------------------------------------- /remotion/round-svg.ts: -------------------------------------------------------------------------------- 1 | import {parsePath, roundCommands} from 'svg-round-corners'; 2 | 3 | const cache: Record = {}; 4 | 5 | export const roundSvg = (d: string, borderRadius: number) => { 6 | if (cache[d]) { 7 | return cache[d]; 8 | } 9 | const parsed = parsePath(d); 10 | cache[d] = roundCommands(parsed, borderRadius).path; 11 | 12 | return cache[d]; 13 | }; 14 | -------------------------------------------------------------------------------- /remotion/theme.tsx: -------------------------------------------------------------------------------- 1 | import {setCookie} from 'cookies-next'; 2 | import Head from 'next/head'; 3 | import React, { 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useMemo, 8 | useState, 9 | } from 'react'; 10 | 11 | export type ThemeId = 'red' | 'golden' | 'blue' | 'green'; 12 | 13 | export type Theme = { 14 | name: ThemeId; 15 | displayName: string; 16 | mainColor: string; 17 | accentColor: string; 18 | background: string; 19 | musicPreview: string; 20 | musicRendering: string; 21 | }; 22 | 23 | export const redTheme: Theme = { 24 | name: 'red', 25 | displayName: 'Candy Dream', 26 | mainColor: '#e74b3c', 27 | accentColor: '#900', 28 | background: '#FFE3CA', 29 | musicPreview: 'music/track-1.mp3', 30 | musicRendering: 'music/track-1.wav', 31 | }; 32 | 33 | export const goldenTheme: Theme = { 34 | name: 'golden', 35 | displayName: 'Funky Gold', 36 | mainColor: '#DAA520', 37 | accentColor: '#C97723', 38 | background: '#f7f1de', 39 | musicPreview: 'music/track-2.mp3', 40 | musicRendering: 'music/track-2.wav', 41 | }; 42 | 43 | export const blueTheme: Theme = { 44 | name: 'blue', 45 | displayName: 'Icy Winter', 46 | mainColor: '#4185de', 47 | accentColor: '#233DC9', 48 | background: '#e0f2fc', 49 | musicPreview: 'music/track-3.mp3', 50 | musicRendering: 'music/track-3.wav', 51 | }; 52 | 53 | export const allThemes = [redTheme, goldenTheme, blueTheme]; 54 | 55 | type Context = { 56 | theme: Theme; 57 | setTheme: (newTheme: Theme) => void; 58 | }; 59 | 60 | export const ThemeContext = createContext({ 61 | theme: redTheme, 62 | setTheme: () => { 63 | throw new Error('no context'); 64 | }, 65 | }); 66 | 67 | export const useTheme = () => { 68 | const {theme, setTheme} = useContext(ThemeContext); 69 | return [theme, setTheme] as const; 70 | }; 71 | 72 | export const DEFAULT_THEME = redTheme; 73 | 74 | export const ThemeProvider: React.FC<{ 75 | children: React.ReactNode; 76 | initialTheme: string | null; 77 | }> = ({children, initialTheme}) => { 78 | const getTheme = useCallback(() => { 79 | const theme = allThemes.find((a) => a.name === initialTheme); 80 | 81 | return theme ?? DEFAULT_THEME; 82 | }, [initialTheme]); 83 | 84 | const [theme, setTheme] = useState(() => getTheme()); 85 | 86 | const data = useMemo(() => { 87 | return { 88 | theme, 89 | setTheme: (t: Theme) => { 90 | persistTheme(t); 91 | setTheme(t); 92 | }, 93 | }; 94 | }, [theme]); 95 | 96 | return ( 97 | 98 | 99 | 100 | 101 | 102 | {children} 103 | 104 | ); 105 | }; 106 | 107 | export const persistTheme = (theme: Theme) => { 108 | setCookie('theme', theme.name); 109 | }; 110 | -------------------------------------------------------------------------------- /remotion/top-language-stairs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TopLanguage} from '../src/get-all'; 3 | import {TopLanguageIcon} from './TopLanguageIcon'; 4 | 5 | export const TopLanguagePodium: React.FC<{ 6 | topLanguages: TopLanguage[]; 7 | delay: number; 8 | }> = ({topLanguages, delay}) => { 9 | const [lang, ...moreLangs] = topLanguages; 10 | return ( 11 | <> 12 | 19 |
27 | {moreLangs.map((language, i) => { 28 | return ( 29 | 37 | ); 38 | })} 39 |
40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /remotion/use-noise-translate.ts: -------------------------------------------------------------------------------- 1 | import {noise2D} from '@remotion/noise'; 2 | import {useCurrentFrame} from 'remotion'; 3 | 4 | export const useNoiseTranslate = (hash: string | undefined) => { 5 | const frame = useCurrentFrame(); 6 | return [ 7 | noise2D(hash + 'x', frame / 100, 0) * 2, 8 | noise2D(hash + 'y', frame / 100, 0) * 2, 9 | ]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/AnimatedRoughBox.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {RoughBox, RoughBoxProps} from './RoughBox'; 3 | 4 | export const AnimatedRoughBox: React.FC = ({ 5 | children, 6 | ...props 7 | }) => { 8 | const [number, setNumber] = useState(1); 9 | 10 | useEffect(() => { 11 | const interval = setInterval(() => { 12 | setNumber((n) => n + 1); 13 | }, 1000); 14 | return () => clearInterval(interval); 15 | }, []); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React, {useMemo} from 'react'; 3 | import {Theme} from '../../remotion/theme'; 4 | import {Bauble} from './Bauble'; 5 | import {GithubIcon} from './Github'; 6 | import {PlayButton} from './Play'; 7 | 8 | export const FOOTER_HEIGHT = 200; 9 | 10 | const container: React.CSSProperties = { 11 | minHeight: FOOTER_HEIGHT, 12 | fontFamily: 'MonaSans', 13 | paddingLeft: 20, 14 | paddingRight: 20, 15 | paddingTop: 40, 16 | paddingBottom: 40, 17 | justifyContent: 'center', 18 | fontSize: 14, 19 | textAlign: 'center', 20 | fontWeight: 500, 21 | display: 'flex', 22 | }; 23 | 24 | const link: React.CSSProperties = { 25 | flex: 1, 26 | }; 27 | 28 | const item: React.CSSProperties = { 29 | flex: 1, 30 | cursor: 'pointer', 31 | display: 'flex', 32 | flexDirection: 'row', 33 | alignItems: 'center', 34 | paddingTop: 5, 35 | paddingBottom: 5, 36 | }; 37 | 38 | export const Footer: React.FC<{ 39 | theme: Theme; 40 | }> = ({theme}) => { 41 | const outer: React.CSSProperties = useMemo( 42 | () => ({ 43 | backgroundColor: theme.background, 44 | }), 45 | [theme.background] 46 | ); 47 | 48 | return ( 49 |
50 |
51 | 57 |
58 | 62 | Made with Remotion 63 |
64 |
65 | 71 |
72 | 76 | Source Code 77 |
78 |
79 | 80 |
81 |
82 | 86 | About this site 87 |
88 |
89 | 90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/Grill.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | import {getRoughGenerator} from '../../remotion/get-rough'; 3 | 4 | export const Grill: React.FC = () => { 5 | const d = `M 0 0 L 100 0 L 100 100 L 0 100 Z`; 6 | const paths = useMemo(() => { 7 | const path = getRoughGenerator(); 8 | const drawable = path.path(d, { 9 | roughness: 0.3, 10 | fill: 'black', 11 | seed: 3, 12 | maxRandomnessOffset: 4, 13 | hachureGap: 4, 14 | strokeWidth: 3, 15 | bowing: 1, 16 | }); 17 | 18 | return path.toPaths(drawable); 19 | }, [d]); 20 | return ( 21 | <> 22 | {paths.map((p) => { 23 | const {d, stroke, strokeWidth, fill} = p; 24 | return ( 25 | 32 | ); 33 | })} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/HomeSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Contribs} from '../../remotion/Contribs'; 3 | import {Bonbon} from '../../remotion/Icons/Bonbon'; 4 | import {useTheme} from '../../remotion/theme'; 5 | import {CameraIcon} from './Camera'; 6 | import {ThemeSwitcherContent} from './ThemeSwitcherContent'; 7 | 8 | const row: React.CSSProperties = { 9 | display: 'flex', 10 | flexDirection: 'row', 11 | alignItems: 'center', 12 | }; 13 | 14 | const spacer: React.CSSProperties = { 15 | width: 24, 16 | }; 17 | 18 | const vSpacer: React.CSSProperties = { 19 | height: 16, 20 | }; 21 | 22 | export const HomeSidebar: React.FC = () => { 23 | const [theme] = useTheme(); 24 | 25 | return ( 26 |
27 |
28 | 34 |
35 |
36 | A whimsical video

made just for you 37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 | Your coding statistics, 46 |
47 | animated 48 |
49 |
50 |
51 |
52 |
53 |
54 | 60 |
61 |
62 | An MP4 video

that you can share 63 |
64 |
65 |
66 |
67 |
68 |
74 | 75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /src/components/RoughBox.tsx: -------------------------------------------------------------------------------- 1 | import {PlayerInternals} from '@remotion/player'; 2 | import React, {useMemo, useRef} from 'react'; 3 | import {getRoughGenerator} from '../../remotion/get-rough'; 4 | 5 | const style: React.CSSProperties = { 6 | backgroundColor: 'white', 7 | }; 8 | 9 | export type RoughBoxProps = { 10 | children: React.ReactNode; 11 | seed: number; 12 | style: React.CSSProperties; 13 | className?: string; 14 | containerClassName?: string; 15 | padding?: number; 16 | roughness?: number; 17 | strokeWidth?: number; 18 | stroke?: string; 19 | }; 20 | 21 | export const RoughBox: React.FC = ({ 22 | children, 23 | seed, 24 | containerClassName, 25 | padding, 26 | className, 27 | roughness, 28 | strokeWidth, 29 | stroke, 30 | style: passedStyle, 31 | }) => { 32 | const ref = useRef(null); 33 | 34 | const elementSize = PlayerInternals.useElementSize(ref, { 35 | shouldApplyCssTransforms: false, 36 | triggerOnWindowResize: false, 37 | }); 38 | 39 | const d = elementSize 40 | ? `M 0 0 L ${elementSize.width} ${0} L ${elementSize.width} ${ 41 | elementSize.height 42 | } L 0 ${elementSize.height} z` 43 | : `M 0 0 `; 44 | 45 | const paths = useMemo(() => { 46 | const path = getRoughGenerator(); 47 | const drawable = path.path(d, { 48 | roughness: roughness ?? 0.7, 49 | seed: seed, 50 | maxRandomnessOffset: 4, 51 | strokeWidth: strokeWidth ?? 5, 52 | stroke: stroke ?? 'black', 53 | bowing: 1, 54 | }); 55 | 56 | return path.toPaths(drawable); 57 | }, [d, roughness, seed, stroke, strokeWidth]); 58 | 59 | const content: React.CSSProperties = useMemo(() => { 60 | return { 61 | padding: padding ?? 20, 62 | }; 63 | }, [padding]); 64 | 65 | return ( 66 |
67 | 76 | {paths.map((p) => { 77 | return ( 78 | 85 | ); 86 | })} 87 | 88 |
89 | {children} 90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {RoughBox} from './RoughBox'; 3 | import {ThemeSwitcherContent} from './ThemeSwitcherContent'; 4 | 5 | const container: React.CSSProperties = { 6 | display: 'flex', 7 | flexDirection: 'row', 8 | justifyContent: 'flex-end', 9 | paddingRight: 20, 10 | fontFamily: 'MonaSans', 11 | alignItems: 'center', 12 | flex: 1, 13 | }; 14 | 15 | export const ThemeSwitcher: React.FC = () => { 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcherContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {allThemes, useTheme} from '../../remotion/theme'; 3 | import {ThemeSwitcherItem} from './ThemeSwitcherItem'; 4 | 5 | const themeTitle: React.CSSProperties = { 6 | fontSize: 13, 7 | marginBottom: 4, 8 | }; 9 | 10 | const themeName: React.CSSProperties = { 11 | fontWeight: 'bold', 12 | width: 130, 13 | }; 14 | 15 | const spacer: React.CSSProperties = { 16 | width: 14, 17 | }; 18 | 19 | export const ThemeSwitcherContent: React.FC = () => { 20 | const [activeTheme, setTheme] = useTheme(); 21 | 22 | return ( 23 | <> 24 |
25 |
Theme
26 |
{activeTheme.displayName}
27 |
28 |
29 | {allThemes.map((theme, i) => { 30 | return ( 31 | setTheme(theme)} 37 | /> 38 | ); 39 | })} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcherItem.tsx: -------------------------------------------------------------------------------- 1 | import {RoughCircleStatic} from '../../remotion/RoughCircleStatic'; 2 | 3 | const SIZE = 40; 4 | 5 | export const ThemeSwitcherItem: React.FC<{ 6 | onClick: () => void; 7 | seed: number; 8 | color: string; 9 | active: boolean; 10 | }> = ({onClick, seed, color, active}) => { 11 | return ( 12 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/Unwrapped.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useTheme} from '../../remotion/theme'; 3 | import {GithubIcon} from './Github'; 4 | 5 | const height = 60; 6 | 7 | export const UnwrappedTitle: React.FC = () => { 8 | const [theme] = useTheme(); 9 | return ( 10 |
25 |
33 | 40 | #GitHubUnwrapped 2022 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/button.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Theme} from '../../remotion/theme'; 3 | 4 | export const button = (theme: Theme): React.CSSProperties => ({ 5 | appearance: 'none', 6 | WebkitAppearance: 'none', 7 | padding: '20px 28px', 8 | border: 0, 9 | color: 'white', 10 | backgroundColor: theme.mainColor, 11 | borderRadius: 5, 12 | fontSize: 20, 13 | fontFamily: 'MonaSans', 14 | cursor: 'pointer', 15 | fontWeight: 700, 16 | }); 17 | 18 | export const backButton: React.CSSProperties = { 19 | textAlign: 'left', 20 | backgroundColor: 'transparent', 21 | color: 'black', 22 | fontSize: 16, 23 | flexDirection: 'row', 24 | alignItems: 'center', 25 | fontWeight: 700, 26 | display: 'flex', 27 | cursor: 'pointer', 28 | }; 29 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import {VERSION} from 'remotion'; 2 | 3 | export const COMP_NAME = 'main'; 4 | export const OG_COMP_NAME = 'OG'; 5 | export const SITE_ID = `unwrapped-2022-${VERSION}`; 6 | export const DURATION = 1150; 7 | export const RAM = 2048; 8 | export const DISK = 2048; 9 | export const TIMEOUT = 240; 10 | 11 | export const DOMAIN = 'https://www.githubunwrapped.com'; 12 | export const DB = 'wrapped2022'; 13 | -------------------------------------------------------------------------------- /src/db/cache.ts: -------------------------------------------------------------------------------- 1 | import {BackendStats, CompactStats} from '../../remotion/map-response-to-stats'; 2 | import {ThemeId} from '../../remotion/theme'; 3 | import {DB} from '../config'; 4 | import {mongoClient} from './mongo'; 5 | 6 | type CacheCollection = { 7 | username: string; 8 | stats: CompactStats; 9 | }; 10 | 11 | type BackendStatsCollection = { 12 | username: string; 13 | backendStats: BackendStats; 14 | }; 15 | 16 | type OgImageCollection = { 17 | username: string; 18 | image: string; 19 | theme: ThemeId; 20 | }; 21 | 22 | type EmailCollection = { 23 | email: string; 24 | }; 25 | 26 | export const allStatscollection = async () => { 27 | const client = await mongoClient; 28 | return client.db(DB).collection('wrapped'); 29 | }; 30 | 31 | export const backendStatsCollection = async () => { 32 | const client = await mongoClient; 33 | return client.db(DB).collection('backendstats'); 34 | }; 35 | 36 | export const dbImageCollection = async () => { 37 | const client = await mongoClient; 38 | return client.db(DB).collection('og'); 39 | }; 40 | 41 | export const dbEmailCollection = async () => { 42 | const client = await mongoClient; 43 | return client.db(DB).collection('email'); 44 | }; 45 | 46 | export const saveEmailAdress = async (email: string) => { 47 | const collection = await dbEmailCollection(); 48 | await collection.updateOne( 49 | { 50 | email: email.toLowerCase(), 51 | }, 52 | { 53 | $set: { 54 | email: email.toLowerCase(), 55 | }, 56 | }, 57 | { 58 | upsert: true, 59 | } 60 | ); 61 | }; 62 | 63 | export const getEmailFromDb = async (email: string) => { 64 | const collection = await dbEmailCollection(); 65 | return collection.findOne({ 66 | email: email.toLowerCase(), 67 | }); 68 | }; 69 | 70 | export const saveOgImage = async ({ 71 | username, 72 | image, 73 | theme, 74 | }: { 75 | username: string; 76 | image: string; 77 | theme: ThemeId; 78 | }) => { 79 | const collection = await dbImageCollection(); 80 | await collection.updateOne( 81 | { 82 | username: username.toLowerCase(), 83 | }, 84 | { 85 | $set: { 86 | username: username.toLowerCase(), 87 | image, 88 | theme, 89 | }, 90 | }, 91 | { 92 | upsert: true, 93 | } 94 | ); 95 | }; 96 | 97 | export const getOgImage = async ({username}: {username: string}) => { 98 | const collection = await dbImageCollection(); 99 | return collection.findOne({ 100 | username: username.toLowerCase(), 101 | }); 102 | }; 103 | 104 | export const saveCache = async ({ 105 | username, 106 | stats, 107 | }: { 108 | username: string; 109 | stats: CompactStats; 110 | }) => { 111 | const coll = await allStatscollection(); 112 | return coll.updateOne( 113 | { 114 | username: username.toLowerCase(), 115 | }, 116 | { 117 | $set: { 118 | stats, 119 | username: username.toLowerCase(), 120 | }, 121 | }, 122 | { 123 | upsert: true, 124 | } 125 | ); 126 | }; 127 | 128 | export const getAllStatsFromCache = async ( 129 | username: string 130 | ): Promise => { 131 | const coll = await allStatscollection(); 132 | const f = await coll.findOne({ 133 | username: username.toLowerCase(), 134 | }); 135 | if (f) { 136 | return f.stats; 137 | } 138 | return null; 139 | }; 140 | -------------------------------------------------------------------------------- /src/db/mongo.ts: -------------------------------------------------------------------------------- 1 | // From Vercel MongoDB starter 2 | 3 | import {MongoClient} from 'mongodb'; 4 | 5 | const uri = process.env.MONGO_URL as string; // your mongodb connection string 6 | const options = {}; 7 | 8 | let client; 9 | let clientPromise: Promise; 10 | 11 | declare global { 12 | var _mongoClientPromise: Promise; 13 | } 14 | if (!process.env.MONGO_URL) { 15 | throw new Error('Please add your Mongo URI to .env'); 16 | } 17 | 18 | if (process.env.NODE_ENV === 'development') { 19 | // In development mode, use a global variable so that the value 20 | // is preserved across module reloads caused by HMR (Hot Module Replacement). 21 | if (!global._mongoClientPromise) { 22 | client = new MongoClient(uri, options); 23 | global._mongoClientPromise = client.connect(); 24 | } 25 | clientPromise = global._mongoClientPromise; 26 | } else { 27 | // In production mode, it's best to not use a global variable. 28 | client = new MongoClient(uri, options); 29 | clientPromise = client.connect(); 30 | } 31 | 32 | // Export a module-scoped MongoClient promise. By doing this in a 33 | // separate module, the client can be shared across functions. 34 | export const mongoClient = clientPromise; 35 | -------------------------------------------------------------------------------- /src/db/renders.ts: -------------------------------------------------------------------------------- 1 | import {AwsRegion} from '@remotion/lambda'; 2 | import {WithId} from 'mongodb'; 3 | import {ThemeId} from '../../remotion/theme'; 4 | import {DB} from '../config'; 5 | import {mongoClient} from './mongo'; 6 | 7 | export type Finality = 8 | | { 9 | type: 'success'; 10 | url: string; 11 | outputSize: number; 12 | } 13 | | { 14 | type: 'error'; 15 | errors: string; 16 | }; 17 | 18 | export type Render = { 19 | renderId: string | null; 20 | region: AwsRegion; 21 | username: string; 22 | theme: ThemeId; 23 | bucketName: string | null; 24 | finality: Finality | null; 25 | functionName: string; 26 | account: number; 27 | }; 28 | 29 | export const rendersCollection = async () => { 30 | const client = await mongoClient; 31 | return client.db(DB).collection('renders'); 32 | }; 33 | 34 | export const lockRender = async ({ 35 | region, 36 | username, 37 | account, 38 | functionName, 39 | theme, 40 | }: { 41 | region: AwsRegion; 42 | username: string; 43 | account: number; 44 | functionName: string; 45 | theme: ThemeId; 46 | }) => { 47 | const coll = await rendersCollection(); 48 | await coll.insertOne({ 49 | region, 50 | username: username.toLowerCase(), 51 | bucketName: null, 52 | finality: null, 53 | renderId: null, 54 | account, 55 | functionName, 56 | theme, 57 | }); 58 | }; 59 | 60 | export const saveRender = async ({ 61 | region, 62 | username, 63 | renderId, 64 | bucketName, 65 | theme, 66 | account, 67 | }: { 68 | region: AwsRegion; 69 | username: string; 70 | renderId: string; 71 | bucketName: string; 72 | theme: ThemeId; 73 | account: number; 74 | }) => { 75 | const coll = await rendersCollection(); 76 | await coll.updateOne( 77 | { 78 | region, 79 | username: username.toLowerCase(), 80 | theme, 81 | }, 82 | { 83 | $set: { 84 | renderId, 85 | bucketName, 86 | finality: null, 87 | region, 88 | account, 89 | }, 90 | } 91 | ); 92 | }; 93 | 94 | export const updateRenderWithFinality = async ({ 95 | username, 96 | region, 97 | finality, 98 | theme, 99 | renderId, 100 | account, 101 | }: { 102 | username: string; 103 | region: AwsRegion; 104 | finality: Finality; 105 | theme: ThemeId; 106 | renderId: string | null; 107 | account: number; 108 | }) => { 109 | if (finality && finality.type === 'success') { 110 | console.log(`Successfully rendered video for ${username}.`); 111 | } else { 112 | console.log(`Failed to render video for ${username}!`); 113 | } 114 | const coll = await rendersCollection(); 115 | return coll.updateOne( 116 | { 117 | theme, 118 | account, 119 | username: username.toLowerCase(), 120 | }, 121 | { 122 | $set: { 123 | finality: finality, 124 | renderId, 125 | region, 126 | }, 127 | } 128 | ); 129 | }; 130 | 131 | export const getRender = async ({ 132 | username, 133 | theme, 134 | }: { 135 | username: string; 136 | theme: ThemeId; 137 | }): Promise | null> => { 138 | const coll = await rendersCollection(); 139 | const render = await coll.findOne({ 140 | username: username.toLowerCase(), 141 | theme, 142 | }); 143 | 144 | return render; 145 | }; 146 | 147 | export const deleteRender = async (render: WithId) => { 148 | const coll = await rendersCollection(); 149 | await coll.deleteOne({ 150 | _id: render._id, 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /src/discord-monitoring.ts: -------------------------------------------------------------------------------- 1 | export const sendDiscordMessage = async (message: string) => { 2 | const channel = process.env.DISCORD_CHANNEL; 3 | const token = process.env.DISCORD_TOKEN; 4 | console.log(message); 5 | if (!channel) { 6 | console.warn('no DISCORD_CHANNEL env variable set.'); 7 | return; 8 | } 9 | if (!token) { 10 | console.warn('no DISCORD_TOKEN env variable set.'); 11 | return; 12 | } 13 | 14 | try { 15 | await fetch(`https://discord.com/api/channels/${channel}/messages`, { 16 | method: 'post', 17 | body: JSON.stringify({ 18 | content: message, 19 | allowed_mentions: {}, 20 | flags: 1 << 2, 21 | }), 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | Authorization: `Bot ${token}`, 25 | }, 26 | }); 27 | } catch (err) { 28 | console.log('failed to send discord message'); 29 | console.log(err); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/get-account-count.ts: -------------------------------------------------------------------------------- 1 | // Define in the .env file as many AWS account credentials as possible: 2 | // | AWS_KEY_1= 3 | // | AWS_SECRET_1= 4 | // | AWS_KEY_2= 5 | // | AWS_SECRET_2= 6 | // This method will count how many there are so they can be rotated between each other. 7 | 8 | export const getAccountCount = () => { 9 | let count = 0; 10 | while ( 11 | process.env['AWS_KEY_' + (count + 1)] && 12 | process.env['AWS_SECRET_' + (count + 1)] 13 | ) { 14 | count++; 15 | } 16 | 17 | return count; 18 | }; 19 | -------------------------------------------------------------------------------- /src/get-random-aws-account.ts: -------------------------------------------------------------------------------- 1 | import {getAccountCount} from './get-account-count'; 2 | 3 | export const getRandomAwsAccount = () => { 4 | return Math.ceil(Math.random() * getAccountCount()); 5 | }; 6 | -------------------------------------------------------------------------------- /src/get-random-github-token.ts: -------------------------------------------------------------------------------- 1 | export const getGithubTokenCount = () => { 2 | let count = 0; 3 | while (process.env['GITHUB_TOKEN_' + (count + 1)]) { 4 | count++; 5 | } 6 | 7 | return count; 8 | }; 9 | 10 | export const getRandomGithubToken = (): string => { 11 | const tokenCount = getGithubTokenCount(); 12 | if (tokenCount === 0) { 13 | throw new Error('no github token set in env file'); 14 | } 15 | const index = Math.ceil(Math.random() * tokenCount); 16 | return process.env['GITHUB_TOKEN_' + index] as string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/get-render-progress-with-finality.ts: -------------------------------------------------------------------------------- 1 | import {getRenderProgress} from '@remotion/lambda/client'; 2 | import {Render, updateRenderWithFinality} from './db/renders'; 3 | import {sendDiscordMessage} from './discord-monitoring'; 4 | import {getFinality} from './get-render-or-make'; 5 | import {setEnvForKey} from './set-env-for-key'; 6 | import {RenderProgressOrFinality} from './types'; 7 | 8 | export const getRenderProgressWithFinality = async ({ 9 | render, 10 | assume0Progress, 11 | }: { 12 | render: Render; 13 | assume0Progress: boolean; 14 | }): Promise => { 15 | setEnvForKey(render.account); 16 | 17 | if (render.finality) { 18 | return { 19 | type: 'finality', 20 | finality: render.finality, 21 | }; 22 | } 23 | 24 | if (!render.renderId || !render.bucketName) { 25 | return { 26 | progress: { 27 | percent: 0, 28 | }, 29 | type: 'progress', 30 | }; 31 | } 32 | 33 | const progress = assume0Progress 34 | ? null 35 | : await getRenderProgress({ 36 | renderId: render.renderId, 37 | bucketName: render.bucketName, 38 | functionName: render.functionName, 39 | region: render.region, 40 | }); 41 | 42 | const finality = progress === null ? null : getFinality(progress); 43 | 44 | if (finality) { 45 | await updateRenderWithFinality({ 46 | renderId: render.renderId, 47 | username: render.username, 48 | region: render.region, 49 | finality, 50 | theme: render.theme, 51 | account: render.account, 52 | }); 53 | sendDiscordMessage( 54 | `Updated ${render.renderId} with finality: ${JSON.stringify(finality)}}` 55 | ); 56 | return { 57 | type: 'finality', 58 | finality, 59 | }; 60 | } 61 | 62 | return { 63 | type: 'progress', 64 | progress: { 65 | percent: progress === null ? 0 : progress.overallProgress, 66 | }, 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/get-times-of-day.ts: -------------------------------------------------------------------------------- 1 | import type {Hour} from '../remotion/AvgCommitsTitle'; 2 | import {Commit} from '../remotion/frontend-stats'; 3 | 4 | export const getTimesOfDay = (items: Commit[]) => { 5 | const i = items.map((i) => i.date); 6 | 7 | const times = i.map((item) => new Date(item).getHours()); 8 | 9 | let hours: {[key in Hour]: number} = { 10 | '5': 0, 11 | '6': 0, 12 | '7': 0, 13 | '8': 0, 14 | '9': 0, 15 | '10': 0, 16 | '11': 0, 17 | '12': 0, 18 | '13': 0, 19 | '14': 0, 20 | '15': 0, 21 | '16': 0, 22 | '17': 0, 23 | '18': 0, 24 | '19': 0, 25 | '20': 0, 26 | '21': 0, 27 | '22': 0, 28 | '23': 0, 29 | '0': 0, 30 | '1': 0, 31 | '2': 0, 32 | '3': 0, 33 | '4': 0, 34 | }; 35 | for (const time of times) { 36 | hours[time as Hour]++; 37 | } 38 | 39 | return hours; 40 | }; 41 | -------------------------------------------------------------------------------- /src/has-enough-data.ts: -------------------------------------------------------------------------------- 1 | import {BackendStats, CompactStats} from '../remotion/map-response-to-stats'; 2 | 3 | type State = 4 | | 'enough-data' 5 | | 'no-contributions' 6 | | 'no-public-contributions' 7 | | 'no-weekdays' 8 | | 'no-best-commits'; 9 | 10 | export const hasEnoughData = (stats: CompactStats): State => { 11 | if (stats.bestCommits.length === 0) { 12 | return 'no-best-commits'; 13 | } 14 | if (stats.weekdays.mostCount === 0) { 15 | return 'no-weekdays'; 16 | } 17 | 18 | return hasEnoughBackendData(stats); 19 | }; 20 | 21 | export const hasEnoughBackendData = (stats: BackendStats): State => { 22 | if (stats.contributionCount === 0) { 23 | return 'no-contributions'; 24 | } 25 | if (stats.repositoriesContributedTo.length === 0) { 26 | return 'no-public-contributions'; 27 | } 28 | if (!stats.topLanguages || stats.topLanguages.length === 0) { 29 | return 'no-public-contributions'; 30 | } 31 | 32 | return 'enough-data'; 33 | }; 34 | -------------------------------------------------------------------------------- /src/og-images.ts: -------------------------------------------------------------------------------- 1 | import { 2 | renderStillOnLambda, 3 | speculateFunctionName, 4 | } from '@remotion/lambda/client'; 5 | import {random} from 'remotion'; 6 | import {allThemes} from '../remotion/theme'; 7 | import {DISK, OG_COMP_NAME, RAM, SITE_ID, TIMEOUT} from './config'; 8 | import {backendStatsCollection, getOgImage, saveOgImage} from './db/cache'; 9 | import {sendDiscordMessage} from './discord-monitoring'; 10 | import {backendResponseToBackendStats, getAll} from './get-all'; 11 | import {getRandomAwsAccount} from './get-random-aws-account'; 12 | import {getRandomGithubToken} from './get-random-github-token'; 13 | import {hasEnoughBackendData} from './has-enough-data'; 14 | import {getRandomRegion} from './regions'; 15 | import {setEnvForKey} from './set-env-for-key'; 16 | import {OgCompProps} from './types'; 17 | 18 | export const getOgImageOrMake = async ({username}: {username: string}) => { 19 | const image = await getOgImage({username}); 20 | if (image) { 21 | return image.image; 22 | } 23 | 24 | const stat = await getStats(username); 25 | const region = getRandomRegion(); 26 | const account = getRandomAwsAccount(); 27 | setEnvForKey(account); 28 | 29 | const theme = allThemes[Math.floor(random(username) * allThemes.length)]; 30 | 31 | const props: OgCompProps = { 32 | isGeneric: false, 33 | theme, 34 | userStats: stat, 35 | }; 36 | 37 | const time = Date.now(); 38 | const {url} = await renderStillOnLambda({ 39 | functionName: speculateFunctionName({ 40 | diskSizeInMb: DISK, 41 | memorySizeInMb: RAM, 42 | timeoutInSeconds: TIMEOUT, 43 | }), 44 | composition: OG_COMP_NAME, 45 | imageFormat: 'png', 46 | inputProps: props, 47 | region, 48 | privacy: 'public', 49 | serveUrl: SITE_ID, 50 | }); 51 | 52 | await saveOgImage({ 53 | image: url, 54 | theme: theme.name, 55 | username, 56 | }); 57 | 58 | sendDiscordMessage( 59 | `Generated image for ${username} with props in ${Date.now() - time}` 60 | ); 61 | 62 | return url; 63 | }; 64 | 65 | const getStats = async (username: string) => { 66 | const entry = await ( 67 | await backendStatsCollection() 68 | ).findOne({ 69 | username: username.toLowerCase(), 70 | }); 71 | 72 | if (entry) { 73 | return entry.backendStats; 74 | } 75 | 76 | const response = await getAll(username, getRandomGithubToken()); 77 | 78 | const backendStats = backendResponseToBackendStats(response); 79 | 80 | if (hasEnoughBackendData(backendStats)) { 81 | (await backendStatsCollection()).insertOne({ 82 | backendStats, 83 | username, 84 | }); 85 | } 86 | 87 | return backendStats; 88 | }; 89 | -------------------------------------------------------------------------------- /src/regions.ts: -------------------------------------------------------------------------------- 1 | import {AwsRegion, getRegions} from '@remotion/lambda'; 2 | 3 | export const getRandomRegion = (): AwsRegion => { 4 | return getRegions()[Math.floor(Math.random() * getRegions().length)]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/set-env-for-key.ts: -------------------------------------------------------------------------------- 1 | export const setEnvForKey = (key: number) => { 2 | process.env.REMOTION_AWS_ACCESS_KEY_ID = process.env[`AWS_KEY_${key}`]; 3 | process.env.REMOTION_AWS_SECRET_ACCESS_KEY = process.env[`AWS_SECRET_${key}`]; 4 | }; 5 | -------------------------------------------------------------------------------- /src/truthy.ts: -------------------------------------------------------------------------------- 1 | type Truthy = T extends false | '' | 0 | null | undefined ? never : T; 2 | 3 | export function truthy(value: T): value is Truthy { 4 | return Boolean(value); 5 | } 6 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {BackendStats, CompactStats} from '../remotion/map-response-to-stats'; 2 | import {Theme, ThemeId} from '../remotion/theme'; 3 | import {Finality} from './db/renders'; 4 | 5 | export type RenderRequest = { 6 | username: string; 7 | compactStats: CompactStats; 8 | theme: ThemeId; 9 | }; 10 | 11 | export type CompProps = { 12 | stats: CompactStats; 13 | theme: Theme; 14 | type: 'portrait' | 'square' | 'landscape'; 15 | }; 16 | 17 | export type ProgressData = { 18 | username: string; 19 | theme: ThemeId; 20 | }; 21 | 22 | export type RenderProgressOrFinality = 23 | | { 24 | type: 'progress'; 25 | progress: { 26 | percent: number; 27 | }; 28 | } 29 | | { 30 | type: 'finality'; 31 | finality: Finality; 32 | }; 33 | 34 | export type OgCompProps = { 35 | userStats: BackendStats; 36 | theme: Theme; 37 | isGeneric: boolean; 38 | }; 39 | 40 | export type EmailResponse = 41 | | { 42 | type: 'success'; 43 | message: string; 44 | } 45 | | { 46 | type: 'error'; 47 | error: string; 48 | }; 49 | -------------------------------------------------------------------------------- /src/use-window-size.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | // Define general type for useWindowSize hook, which includes width and height 3 | interface Size { 4 | width: number | undefined; 5 | height: number | undefined; 6 | } 7 | 8 | // Hook 9 | export function useWindowSize(): Size { 10 | // Initialize state with undefined width/height so server and client renders match 11 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 12 | const [windowSize, setWindowSize] = useState({ 13 | width: undefined, 14 | height: undefined, 15 | }); 16 | useEffect(() => { 17 | // Handler to call on window resize 18 | function handleResize() { 19 | // Set window width/height to state 20 | setWindowSize({ 21 | width: window.innerWidth, 22 | height: window.innerHeight, 23 | }); 24 | } 25 | // Add event listener 26 | document.addEventListener('resize', handleResize); 27 | // Call handler right away so state gets updated with initial window size 28 | handleResize(); 29 | // Remove event listener on cleanup 30 | return () => document.removeEventListener('resize', handleResize); 31 | }, []); // Empty array ensures that effect is only run on mount 32 | return windowSize; 33 | } 34 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'MonaSans'; 3 | src: url(/Mona-Sans-Medium.otf); 4 | font-weight: 500; 5 | } 6 | 7 | @font-face { 8 | font-family: 'MonaSans'; 9 | src: url(/Mona-Sans-Bold.otf); 10 | font-weight: 700; 11 | } 12 | 13 | @font-face { 14 | font-family: 'MonaSans'; 15 | src: url(/Mona-Sans-ExtraBold.otf); 16 | font-weight: 900; 17 | } 18 | 19 | html, 20 | body { 21 | padding: 0; 22 | margin: 0; 23 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 24 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 25 | } 26 | 27 | a { 28 | color: inherit; 29 | text-decoration: none; 30 | } 31 | 32 | * { 33 | box-sizing: border-box; 34 | } 35 | 36 | .github-username:focus { 37 | outline: none; 38 | } 39 | 40 | .mobile-row { 41 | flex-direction: row; 42 | } 43 | 44 | .play-button { 45 | height: 200px; 46 | width: 200px; 47 | } 48 | 49 | .play-button-icon { 50 | height: 60px; 51 | } 52 | 53 | .footer { 54 | align-items: center; 55 | } 56 | 57 | .right-container { 58 | margin-left: 80px; 59 | } 60 | 61 | .header-style { 62 | align-items: center; 63 | } 64 | 65 | .footer-item { 66 | justify-content: center; 67 | } 68 | 69 | .container-1000 { 70 | max-width: 1000px; 71 | margin: auto; 72 | } 73 | 74 | .pad { 75 | padding-left: 20px; 76 | padding-right: 20px; 77 | } 78 | 79 | .unwrapped-title { 80 | font-size: 30px; 81 | width: 500px; 82 | } 83 | 84 | .input-style { 85 | min-width: 400px; 86 | } 87 | 88 | @media screen and (max-width: 1000px) { 89 | .mobile-row { 90 | flex-direction: column; 91 | } 92 | .mobile-full-width { 93 | width: 100%; 94 | } 95 | .mobile-flex { 96 | flex: 1 97 | } 98 | .play-button { 99 | height: 150px; 100 | width: 150px; 101 | } 102 | .play-button-icon { 103 | height: 40px; 104 | } 105 | .footer { 106 | align-items: flex-start; 107 | } 108 | .right-container { 109 | margin-left: 0; 110 | margin-top: 70px; 111 | } 112 | .header-style { 113 | align-items: flex-start; 114 | } 115 | .footer-item { 116 | justify-content: flex-start; 117 | } 118 | .container-1000 { 119 | max-width: 500px; 120 | margin: auto; 121 | } 122 | .unwrapped-title { 123 | font-size: 20px; 124 | width: 100%; 125 | } 126 | .input-style { 127 | min-width: 100%; 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /test/tree.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from 'vitest'; 2 | import { 3 | getIndicesToClose, 4 | getTreeMath, 5 | makeIndicesAccurate, 6 | } from '../remotion/tree/indices-to-close'; 7 | 8 | test('Should calculate tree correctly', () => { 9 | const { 10 | avgDotsPerRow: avgRotsPerRow, 11 | dotsPerRow, 12 | rows, 13 | } = getTreeMath({ 14 | height: 1080, 15 | issuesClosed: 0, 16 | issuesOpen: 2, 17 | width: 1080, 18 | }); 19 | const indices = makeIndicesAccurate({ 20 | indices: getIndicesToClose({ 21 | avgDotsPerRow: avgRotsPerRow, 22 | dotsPerRow, 23 | rows, 24 | totalIssues: 2, 25 | }), 26 | expectedIndices: 0, 27 | totalIssues: 2, 28 | }); 29 | 30 | expect(indices).toEqual([]); 31 | }); 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "noUnusedParameters": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "noUnusedLocals": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"], 22 | "ts-node": { 23 | "compilerOptions": { 24 | "module": "commonjs" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/*'], 6 | }, 7 | }); 8 | --------------------------------------------------------------------------------