mirror of
https://github.com/tomru/advcal.git
synced 2026-03-02 22:17:17 +01:00
first working version
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,3 +32,5 @@ yarn-error.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
.tool-versions
|
||||
.log
|
||||
|
||||
31
README.md
31
README.md
@@ -1,30 +1,7 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
# Bello's Adventskalender
|
||||
|
||||
## Getting Started
|
||||
# Getting Youtube playlist data
|
||||
|
||||
First, run the development server:
|
||||
Get data in the [Api Explorer](https://developers.google.com/youtube/v3/docs/playlistItems/list?apix_params=%7B%22part%22%3A%5B%22snippet%22%5D%2C%22maxResults%22%3A24%2C%22playlistId%22%3A%22PLrhIlImke6F4uwg70DQmzGFOKIZdTpwPv%22%7D#usage)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
Then pipe it to `./scripts/getSongs.sh` and copy it into `./pages/api/songs.js`
|
||||
|
||||
42
components/calendar.js
Normal file
42
components/calendar.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import AppContext from "../context/app";
|
||||
|
||||
const Calendar = () => {
|
||||
const { loading, songs, openDoor } = useContext(AppContext);
|
||||
|
||||
if (loading || !songs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul className="cal">
|
||||
{songs.map((song, index) => {
|
||||
const classes = ["calcard", song.locked && "calcard__locked"]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classes}
|
||||
data-id={index}
|
||||
key={song.id}
|
||||
onClick={() => !song.locked && openDoor(index)}
|
||||
title={
|
||||
song.locked &&
|
||||
`Es ist noch nicht der ${new Date(
|
||||
song.lockedUntil
|
||||
).toLocaleDateString()}! Geduld, nur Gedult!` || index + 1
|
||||
}
|
||||
>
|
||||
<span className="cardnumber">{index + 1}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
36
components/controls.js
vendored
Normal file
36
components/controls.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useEffect, useState, Fragment } from "react";
|
||||
|
||||
export const ENDED = 0;
|
||||
export const PLAYING = 1;
|
||||
export const PAUSED = 2;
|
||||
|
||||
const Controls = ({ playerState, onPause, onPlay, onRestart }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="player-controls">
|
||||
{playerState === PLAYING && (
|
||||
<a onClick={onPause} title="Pause">
|
||||
⏸
|
||||
</a>
|
||||
)}
|
||||
{playerState === PAUSED && (
|
||||
<a onClick={onPlay} title="Play">
|
||||
▶
|
||||
</a>
|
||||
)}
|
||||
{[PLAYING, PAUSED].includes(playerState) && (
|
||||
<a onClick={onRestart} title="Von Vorne">
|
||||
⏮
|
||||
</a>
|
||||
)}
|
||||
{playerState === ENDED && (
|
||||
<a onClick={onPlay} title="Play">
|
||||
▶
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Controls;
|
||||
27
components/header.js
Normal file
27
components/header.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const Header = () => {
|
||||
const [gifUrl, setGifUrl] = useState(null);
|
||||
useEffect(() => {
|
||||
async function getGif() {
|
||||
const response = await fetch(
|
||||
"//api.giphy.com/v1/gifs/random?tag=christmas&api_key=3ziHSa4ptYJdv2dOuawgzpBhhiW09Ss1"
|
||||
);
|
||||
const { data } = await response.json();
|
||||
setGifUrl(data.image_mp4_url);
|
||||
}
|
||||
|
||||
getGif();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<h1>Bello's Adventskalender 2020</h1>
|
||||
{gifUrl && (
|
||||
<video className="yay-gif-video" src={gifUrl} autoPlay loop muted />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
28
components/nav.js
Normal file
28
components/nav.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
const links = [
|
||||
{ href: "https://youtube.com", label: "Youtube" },
|
||||
{ href: "https://www.discogs.com/", label: "Discog" },
|
||||
{ href: "https://findmusicbylyrics.com/", label: "Find music by Lyrics" },
|
||||
{ href: "https://musicbrainz.org/", label: "MusicBrainz" }
|
||||
].map(link => {
|
||||
link.key = `nav-link-${link.href}-${link.label}`;
|
||||
return link;
|
||||
});
|
||||
|
||||
const Nav = () => (
|
||||
<nav className="ad-nav">
|
||||
<ul>
|
||||
<li>Für die Recherche:</li>
|
||||
{links.map(({ key, href, label }) => (
|
||||
<li key={key}>
|
||||
<a href={href} target="_blank">
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
||||
export default Nav;
|
||||
64
components/player.js
Normal file
64
components/player.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState, Fragment, useContext } from "react";
|
||||
import youtubePlayer from "youtube-player";
|
||||
|
||||
import AppContext from "../context/app";
|
||||
|
||||
import Controls, { ENDED, PLAYING, PAUSED } from "./controls";
|
||||
|
||||
let player;
|
||||
|
||||
const Player = ({ hidden }) => {
|
||||
const [playerState, setPlayerState] = useState(-1);
|
||||
|
||||
const { openSong } = useContext(AppContext);
|
||||
|
||||
const onPause = () => player && player.pauseVideo();
|
||||
const onPlay = () => player && player.playVideo();
|
||||
const onRestart = () => {
|
||||
player && player.seekTo(openSong.startSeconds || 0);
|
||||
player && [PAUSED, ENDED].includes(playerState) && player.playVideo();
|
||||
};
|
||||
|
||||
useEffect(onRestart, [!hidden]);
|
||||
|
||||
useEffect(() => {
|
||||
const setState = ({ data }) => setPlayerState(data);
|
||||
player = youtubePlayer("player", {
|
||||
height: "390",
|
||||
width: "640"
|
||||
});
|
||||
player.on("stateChange", setState);
|
||||
player.loadVideoById({
|
||||
videoId: openSong.id,
|
||||
startSeconds: openSong.startSeconds,
|
||||
endSeconds: openSong.endSeconds
|
||||
});
|
||||
|
||||
return () => {
|
||||
player = null;
|
||||
setPlayerState(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={hidden ? "hidden" : undefined}>
|
||||
<div id="player" />
|
||||
</div>
|
||||
{hidden && playerState === -1 && <span>🎶 Titel laden...</span>}
|
||||
{hidden && playerState > -1 && (
|
||||
<Fragment>
|
||||
<span>🎶 Titel spielt</span>
|
||||
<Controls
|
||||
playerState={playerState}
|
||||
onPause={onPause}
|
||||
onPlay={onPlay}
|
||||
onRestart={onRestart}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Player;
|
||||
57
components/showdoordialog.js
Normal file
57
components/showdoordialog.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import AppContext from "../context/app";
|
||||
import Player from "./player";
|
||||
|
||||
const ShowDoorDialog = () => {
|
||||
const { loading, openDoor, openSong, openSongIndex } = useContext(AppContext);
|
||||
const [showSolution, setShowSolution] = useState(false);
|
||||
const [showHint, setShowHint] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
openDoor(null);
|
||||
setShowSolution(false);
|
||||
};
|
||||
|
||||
if (loading || !openSong) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { title, hint, id } = openSong;
|
||||
|
||||
return (
|
||||
<div className="door-mask" onClick={handleClose}>
|
||||
<div className="door" onClick={event => event.stopPropagation()}>
|
||||
<a className="door-close" onClick={handleClose}>
|
||||
❌
|
||||
</a>
|
||||
<h1>Türchen {openSongIndex + 1}</h1>
|
||||
<Player hidden={!showSolution} />
|
||||
{!showSolution && (
|
||||
<div>
|
||||
{hint && !showHint && (
|
||||
<p>
|
||||
<a className="link" onClick={() => setShowHint(true)}>
|
||||
Ich brauche einen Tip!
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{hint && showHint && (
|
||||
<div className="hint">
|
||||
<h4>Tip:</h4>
|
||||
<p>{hint}</p>
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
<a className="solve" onClick={() => setShowSolution(true)}>
|
||||
Zeig mir die Lösung!
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowDoorDialog;
|
||||
5
context/app.js
Normal file
5
context/app.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
const AppContext = React.createContext();
|
||||
|
||||
export default AppContext;
|
||||
50
context/appProvider.js
Normal file
50
context/appProvider.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import AppContext from "./app";
|
||||
|
||||
const AppProvider = ({ debug, children }) => {
|
||||
const [songs, setSongs] = useState(null);
|
||||
const [openSong, setOpenSong] = useState(null);
|
||||
|
||||
const URL = debug ? "/api/songs?unlock=true" : "/api/songs";
|
||||
|
||||
useEffect(() => {
|
||||
async function getSongs() {
|
||||
const response = await fetch(URL);
|
||||
|
||||
if (!response.ok) {
|
||||
setError(`HTTP Status of youtube request: ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { songs } = await response.json();
|
||||
setSongs(songs);
|
||||
}
|
||||
|
||||
getSongs();
|
||||
}, []);
|
||||
|
||||
const openDoor = index => {
|
||||
if (!Number.isInteger(index) || !songs[index]) {
|
||||
setOpenSong(null);
|
||||
}
|
||||
|
||||
setOpenSong(songs[index]);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
loading: !songs,
|
||||
songs,
|
||||
openSong,
|
||||
openSongIndex: songs && songs.indexOf(openSong),
|
||||
openDoor
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppProvider;
|
||||
2
next-env.d.ts
vendored
Normal file
2
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
914
package-lock.json
generated
914
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,14 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^1.25.0",
|
||||
"next": "10.0.3",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-nested": "^5.0.1",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1"
|
||||
"react-dom": "17.0.1",
|
||||
"youtube-player": "^5.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.10",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../styles/globals.css'
|
||||
import "../styles/styles.css";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
|
||||
export default (req, res) => {
|
||||
res.statusCode = 200
|
||||
res.json({ name: 'John Doe' })
|
||||
}
|
||||
125
pages/api/songs.js
Normal file
125
pages/api/songs.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const { DateTime } = require("luxon");
|
||||
|
||||
const SONGS = [
|
||||
{
|
||||
id: "Bys-OE_C7lQ",
|
||||
title: "A Christmas Twist - Si Cranstoun",
|
||||
startSeconds: 4,
|
||||
},
|
||||
{
|
||||
id: "lheIRyhsUSE",
|
||||
title: "EAV - Fata Morgana 1986",
|
||||
},
|
||||
{
|
||||
id: "Dqn01vCQnUs",
|
||||
title: "Sheena Easton - My Baby Takes The Morning Train (with Lyric)HQ",
|
||||
},
|
||||
{
|
||||
id: "b_BzMuOAKUs",
|
||||
title: "Rod Stewart - Have I Told You Lately",
|
||||
},
|
||||
{
|
||||
id: "3D-j9PcOPAk",
|
||||
title: "Deifedanz",
|
||||
},
|
||||
{
|
||||
id: "9HvpIgHBSdo",
|
||||
title: 'Gregory Porter - "Be Good (Lion\'s Song)" Official Video',
|
||||
},
|
||||
{
|
||||
id: "3FKA-3uRdQY",
|
||||
title:
|
||||
"Doris Day - Whatever Will Be Will Be Que Sera Sera (Best All Time Hits Forever 2014 / HQ) Mu©o",
|
||||
},
|
||||
{
|
||||
id: "WIoHSu5v1Mo",
|
||||
title: "The Specials - Message to you Rudy",
|
||||
},
|
||||
{
|
||||
id: "6ZwjdGSqO0k",
|
||||
title: "George Harrison - Got My Mind Set On You (Version I)",
|
||||
},
|
||||
{
|
||||
id: "lAj-Q_W9AT4",
|
||||
title: "George Strait - Write This Down (Closed Captioned)",
|
||||
},
|
||||
{
|
||||
id: "YiadNVhaGwk",
|
||||
title: "Chuck Berry - Run Rudolph Run (Official Video)",
|
||||
},
|
||||
{
|
||||
id: "_6FBfAQ-NDE",
|
||||
title: "Depeche Mode - Just Can't Get Enough (Official Video)",
|
||||
},
|
||||
{
|
||||
id: "X22vAmpZSdY",
|
||||
title: "MIEKE TELKAMP ★★★ TULPEN AUS AMSTERDAM ★★★ 1961",
|
||||
},
|
||||
{
|
||||
id: "uvGvmsLQaHA",
|
||||
title: "Elvis Presley - Green Green Grass Of Home (best video)",
|
||||
},
|
||||
{
|
||||
id: "5vheNbQlsyU",
|
||||
title:
|
||||
"Lady Gaga - Always Remember Us This Way (from A Star Is Born) (Official Music Video)",
|
||||
},
|
||||
{
|
||||
id: "4WM_R-6AKHE",
|
||||
title: "Mockingbird - Carly Simon & James Taylor",
|
||||
},
|
||||
{
|
||||
id: "MgIwLeASnkw",
|
||||
title: "Elmo & Patsy - Grandma Got Run over by a Reindeer",
|
||||
},
|
||||
{
|
||||
id: "SLErVV1TxUI",
|
||||
title: "Gänsehaut - Karl der Käfer 1983",
|
||||
},
|
||||
{
|
||||
id: "js-2cqqY1K8",
|
||||
title: "The Beautiful South - Perfect 10 (Official Video)",
|
||||
},
|
||||
{
|
||||
id: "S6nSpBiekzQ",
|
||||
title: "Les Négresses Vertes - Zobi La Mouche (Official Music Video)",
|
||||
},
|
||||
{
|
||||
id: "z0qW9P-uYfM",
|
||||
title: "Elton John - Don't Go Breaking My Heart (with Kiki Dee)",
|
||||
},
|
||||
{
|
||||
id: "Pgqa3cVOxUc",
|
||||
title: "The Undertones - My Perfect Cousin (Official Video)",
|
||||
},
|
||||
{
|
||||
id: "lLLL1KxpYMA",
|
||||
title: "Madness - Night Boat to Cairo (Official HD Video)",
|
||||
},
|
||||
{
|
||||
id: "qTx-sdR6Yzk",
|
||||
title: 'Dropkick Murphys - "The Season\'s Upon Us" (Video)',
|
||||
},
|
||||
];
|
||||
|
||||
export default async (req, res) => {
|
||||
const unlocked = !!req.query.unlock;
|
||||
const NOW = DateTime.utc().setZone("Europe/Berlin");
|
||||
|
||||
const getLockedData = (index) => {
|
||||
const dayString = ("00" + (index + 1)).substr(-2, 2);
|
||||
const lockedUntilDateTime = DateTime.fromISO(
|
||||
`${NOW.year}-12-${dayString}T00:00:00.000+01:00`
|
||||
);
|
||||
return {
|
||||
locked: unlocked ? false : NOW < lockedUntilDateTime,
|
||||
lockedUntil: lockedUntilDateTime.toISO(),
|
||||
};
|
||||
};
|
||||
|
||||
const songs = SONGS.map((item, index) => ({
|
||||
...item,
|
||||
...getLockedData(index),
|
||||
}));
|
||||
res.json({ songs });
|
||||
};
|
||||
8
pages/debug.js
Normal file
8
pages/debug.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import Home from "./index";
|
||||
|
||||
const HomeDebug = () => {
|
||||
return <Home debug={true} />;
|
||||
};
|
||||
|
||||
export default HomeDebug;
|
||||
@@ -1,65 +1,25 @@
|
||||
import Head from 'next/head'
|
||||
import styles from '../styles/Home.module.css'
|
||||
import Head from "next/head";
|
||||
|
||||
export default function Home() {
|
||||
import AppProvider from "../context/appProvider";
|
||||
|
||||
import Header from "../components/header";
|
||||
import Calendar from "../components/calendar";
|
||||
import ShowDoorDialog from "../components/showdoordialog";
|
||||
import Nav from "../components/nav";
|
||||
|
||||
export default function Home({ debug = false }) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<title>Bello's Adventskalender 2020</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className={styles.main}>
|
||||
<h1 className={styles.title}>
|
||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
||||
</h1>
|
||||
|
||||
<p className={styles.description}>
|
||||
Get started by editing{' '}
|
||||
<code className={styles.code}>pages/index.js</code>
|
||||
</p>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
||||
<h3>Documentation →</h3>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
||||
<h3>Learn →</h3>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/vercel/next.js/tree/master/examples"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Examples →</h3>
|
||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/import?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Deploy →</h3>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
<AppProvider debug={debug}>
|
||||
<Header />
|
||||
<Calendar />
|
||||
<ShowDoorDialog />
|
||||
<Nav />
|
||||
</AppProvider>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className={styles.footer}>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
18
postcss.config.json
Normal file
18
postcss.config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"plugins": [
|
||||
"postcss-nested",
|
||||
"postcss-flexbugs-fixes",
|
||||
[
|
||||
"postcss-preset-env",
|
||||
{
|
||||
"autoprefixer": {
|
||||
"flexbox": "no-2009"
|
||||
},
|
||||
"stage": 3,
|
||||
"features": {
|
||||
"custom-properties": false
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
5
scripts/getSongs.sh
Executable file
5
scripts/getSongs.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Gets videoIds from a youtube playlistItems response provided on stdin.
|
||||
|
||||
jq -M '[.items[] | {id: .snippet.resourceId.videoId, title: .snippet.title }]' <&0
|
||||
@@ -1,122 +0,0 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-basis: 45%;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
180
styles/styles.css
Normal file
180
styles/styles.css
Normal file
@@ -0,0 +1,180 @@
|
||||
:root {
|
||||
--bg-color: #1b5e20;
|
||||
|
||||
--text-color: #fff;
|
||||
|
||||
--link-color: var(--text-color);
|
||||
--link-color--hover: #000;
|
||||
|
||||
--calcard-color: #fff;
|
||||
--calcard-bg-color: #c62828;
|
||||
--calcard-bg-color__locked: #003300;
|
||||
|
||||
--door-bg-color: #4c8c4a;
|
||||
--door-color: #fff;
|
||||
|
||||
--player-controls-bg-color: #1b5e20;
|
||||
|
||||
--door-hint-bg-color: #c62828;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Charm", cursive;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem 12rem;
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color--hover);
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
position: absolute;
|
||||
left: -5000px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
flex-grow: 1;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.yay-gif-video {
|
||||
max-height: 12rem;
|
||||
max-width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cal {
|
||||
padding-left: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-rows: repeat(4, 6vw);
|
||||
grid-gap: 2rem;
|
||||
|
||||
.calcard {
|
||||
color: var(--calcard-color);
|
||||
background-color: var(--calcard-bg-color);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 6px;
|
||||
box-shadow: 2px 2px 8px 3px #000;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&__locked {
|
||||
background-color: var(--calcard-bg-color__locked);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
& .cardnumber {
|
||||
font-size: 6vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.door-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.door {
|
||||
background-color: var(--door-bg-color);
|
||||
color: var(--door-color);
|
||||
|
||||
padding: 2rem;
|
||||
|
||||
text-align: center;
|
||||
font-size: 1.7rem;
|
||||
|
||||
border-radius: 6px;
|
||||
box-shadow: 2px 2px 8px 3px #000;
|
||||
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
min-width: 30rem;
|
||||
|
||||
.solve {
|
||||
font-size: 2rem;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
|
||||
text-align: left;
|
||||
|
||||
background-color: var(--door-hint-bg-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 8px 3px #000;
|
||||
}
|
||||
|
||||
.door-close {
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.player-controls {
|
||||
display: inline-flex;
|
||||
|
||||
a {
|
||||
width: 2rem;
|
||||
margin-left: 16px;
|
||||
padding: 0 6px;
|
||||
background-color: var(--player-controls-bg-color);
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 3px 2px #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ad-nav {
|
||||
& nav {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& nav > ul {
|
||||
padding: 4px 16px;
|
||||
}
|
||||
|
||||
& li {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user