├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── BackgroundProvider.tsx ├── Card │ ├── CardContent.tsx │ ├── CardDetail.tsx │ ├── CardPreview.tsx │ └── CardReflection.tsx ├── Layouts │ ├── Header.tsx │ └── Layout.tsx ├── PageInternal │ ├── Oracle.tsx │ ├── Protocol.tsx │ └── TextLayouts.tsx └── types.d.ts ├── data ├── backgrounds.ts ├── prompts.ts ├── protocols.ts ├── server.ts ├── suitIllustrations.ts └── times.ts ├── lib └── textTransform.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [id].tsx ├── _app.tsx ├── _document.tsx ├── about.tsx ├── all.tsx └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── styles ├── globals.css └── tarotCard.css └── tsconfig.json /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 1:1, intimate protocols is a project that explores new forms of online intimacy between two people. 2 | It was created by Alice Yuan Zhang and soft networks. It is built in next.js. -------------------------------------------------------------------------------- /components/BackgroundProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { useState } from "react"; 3 | import bgMapping from "../data/backgrounds"; 4 | import { BG_NUM_STAGES, BG_TRANSITION_TIME } from "../data/times"; 5 | import { AsciiRender, BGWIDTH } from "../lib/textTransform"; 6 | 7 | /* Create a context that accepts a background ID and just returns a string */ 8 | 9 | export interface BackgroundContextType { 10 | setBGID: (id: string) => void; 11 | } 12 | 13 | export const BackgroundContext = React.createContext({ 14 | setBGID: () => {}, 15 | }); 16 | 17 | const BackgroundProvider: React.FC = ({ children }) => { 18 | const [bgID, setBGID] = React.useState(""); 19 | const [bgStringNew, setBGStringNew] = React.useState(""); 20 | const [bgStringCurrent, setBgStringCurrent] = React.useState(bgMapping["blank"]); 21 | const [transitionProbability, setTransitionProbability] = React.useState(0.0); 22 | const currentTransition = useRef(null); 23 | 24 | useEffect(() => { 25 | return () => { 26 | if (currentTransition.current) clearTimeout(currentTransition.current); 27 | }; 28 | }, []); 29 | 30 | useEffect(() => { 31 | console.log("Setting background to", bgID); 32 | let newBGString = ""; 33 | if (!bgID || !bgMapping[bgID]) { 34 | newBGString = bgMapping["blank"]; 35 | } else { 36 | newBGString = bgMapping[bgID] || ""; 37 | } 38 | 39 | setBGStringNew(newBGString); 40 | setTransitionProbability(0.0); 41 | }, [bgID]); 42 | 43 | useEffect(() => { 44 | if (transitionProbability > 1) { 45 | return; 46 | } 47 | setBgStringCurrent((s) => { 48 | let lines1 = s.split("\n"); 49 | let lines2 = bgStringNew.split("\n"); 50 | let newStrings = []; 51 | for (let i = 0; i < lines1.length; i++) { 52 | let l = lines1[i]; 53 | let l2 = lines2[i] || ""; 54 | let ns = ""; 55 | for (let j = 0; j < BGWIDTH; j++) { 56 | if (Math.random() < transitionProbability) { 57 | ns += l2[j] || " "; 58 | } else { 59 | ns += l[j] || " "; 60 | } 61 | } 62 | newStrings.push(ns); 63 | } 64 | return newStrings.join("\n"); 65 | }); 66 | currentTransition.current = setTimeout(() => { 67 | setTransitionProbability(transitionProbability + 1 / BG_NUM_STAGES); 68 | }, BG_TRANSITION_TIME / BG_NUM_STAGES); 69 | 70 | // eslint-disable-next-line react-hooks/exhaustive-deps 71 | }, [transitionProbability, bgStringNew]); 72 | 73 | const BackgroundComponent = ( 74 | 75 |
76 |
77 | 78 |
79 | {children} 80 |
81 |
82 | ); 83 | 84 | return BackgroundComponent; 85 | }; 86 | 87 | export const useSetBackgroundID = () => { 88 | const { setBGID } = React.useContext(BackgroundContext); 89 | return setBGID; 90 | }; 91 | 92 | export default BackgroundProvider; 93 | -------------------------------------------------------------------------------- /components/Card/CardContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import suitMapping from "../../data/suitIllustrations"; 3 | import { AsciiRender, textToP } from "../../lib/textTransform"; 4 | import copy from "copy-to-clipboard"; 5 | import { ratingRenderer } from "../PageInternal/TextLayouts"; 6 | import CardReflection from "./CardReflection"; 7 | import { getRatings } from "../../data/server"; 8 | 9 | type CardSideInternal = exerciseProps & { onCardClick?: () => void; preview?: boolean }; 10 | type CardContentInternal = CardSideInternal & { flipCard: number }; 11 | 12 | const CardContent: React.FunctionComponent = (props) => { 13 | return ( 14 |
15 | {!props.flipCard ? ( 16 | 17 | 18 | 19 | ) : ( 20 | "" 21 | )} 22 | {props.flipCard ? ( 23 | 24 | 25 | 26 | ) : ( 27 | "" 28 | )} 29 |
30 | ); 31 | }; 32 | 33 | const CardWrapper: React.FC = ({ preview, children }) => { 34 | return
{children}
; 35 | }; 36 | 37 | const CardActionWrapper: React.FC = ({ children }) => { 38 | return
{children}
; 39 | }; 40 | 41 | const CardFront: React.FC = ({ exercise, onCardClick, preview }) => { 42 | const [wasCopied, setWasCopied] = useState(false); 43 | const [showReflection, setShowReflection] = useState(false); 44 | 45 | const [rating, setRating] = useState(exercise.rating); 46 | 47 | useEffect(() => { 48 | getRatings(exercise.id) 49 | .then((res) => res.json()) 50 | .then((res) => { 51 | console.log("Received rating", res); 52 | if (res.effort && res.intimacy) { 53 | setRating({ effort: res.effort, intimacy: res.intimacy }); 54 | } 55 | }).catch(e => console.log(e)); 56 | }, [exercise.id]); 57 | 58 | useEffect(() => { 59 | if (wasCopied) { 60 | setTimeout(() => setWasCopied(false), 2000); 61 | } 62 | }, [wasCopied]); 63 | return ( 64 | <> 65 | {showReflection ? setShowReflection(false)} /> : ""} 66 |
67 |
Protocol #{exercise.index}
68 |
69 |
{exercise.name}
70 |
{textToP(exercise.text.split("\n"))}
71 |
72 |
73 | {ratingRenderer("#", rating.intimacy)} 74 | {ratingRenderer("@", rating.effort)} 75 |
76 |
77 | {!preview ? ( 78 | 79 | {!wasCopied ? ( 80 |

81 | copy(window.location.host + "/" + exercise.id) && setWasCopied(true)} 84 | > 85 | share 86 | {" "} 87 | this card with your peer 88 |

89 | ) : ( 90 |

link to the card was copied to your clipboard!

91 | )} 92 |

93 | setShowReflection(true)}> 94 | reflect 95 | {" "} 96 | on this protocol afterwards to note level of intimacy (#) and effort (@) 97 |

98 |
99 | ) : null} 100 | 101 | ); 102 | }; 103 | 104 | const CardBack: React.FC = ({ exercise, onCardClick, preview }) => { 105 | return ( 106 | <> 107 |
108 | 109 |
110 | {!preview ? ( 111 | 112 |
113 | 114 | FLIP CARD 115 | {" "} 116 | to reveal 117 |
118 |
119 | ) : null} 120 | 121 | ); 122 | }; 123 | 124 | export default CardContent; 125 | -------------------------------------------------------------------------------- /components/Card/CardDetail.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import CardContent from "./CardContent"; 3 | 4 | const CardDetail: React.FunctionComponent = (props) => { 5 | const [flipCard, setflipCard] = useState(0); 6 | return ( 7 |
8 |
9 | setflipCard(flipCard == 0 ? 1 : 0)}/> 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default CardDetail; -------------------------------------------------------------------------------- /components/Card/CardPreview.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import VisibilitySensor from "react-visibility-sensor"; 4 | import CardContent from "./CardContent"; 5 | 6 | 7 | const CardPreview : React.FC = ( props) => { 8 | 9 | const router = useRouter(); 10 | const myRef = useRef(null); 11 | 12 | const [delayPassed, setDelayPassed] = useState(false); 13 | const [isVisible, setIsVisible] = useState(false); 14 | 15 | useEffect(() => { 16 | let timeout = setTimeout(() => setDelayPassed(true), 300 * ( (props.i || 0) + 1)); 17 | return () => clearTimeout(timeout); 18 | }, [setDelayPassed, props.i]) 19 | 20 | return ( 21 | { 24 | if (becameVisible && !isVisible) setIsVisible(true); 25 | }} 26 | > 27 |
28 | router.push("/" + props.exercise.id)} 32 | preview={true} 33 | /> 34 |
35 |
36 | ); 37 | } 38 | 39 | export default CardPreview; -------------------------------------------------------------------------------- /components/Card/CardReflection.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ReflectionText } from "../../data/prompts"; 3 | import { postRating } from "../../data/server"; 4 | import { textToP } from "../../lib/textTransform"; 5 | import { ratingRenderer } from "../PageInternal/TextLayouts"; 6 | 7 | 8 | interface CardReflectionProps { 9 | exercise: Exercise; 10 | onComplete: () => void 11 | } 12 | 13 | 14 | const CardReflection : React.FC = ({ exercise, onComplete}) => { 15 | 16 | const [numIntimacy, setNumIntimacy] = useState(1); 17 | const [numEffort, setNumEffort] = useState(1); 18 | 19 | return ( 20 |
21 |
22 | {textToP(ReflectionText)} 23 |
24 | 25 |
26 |
setNumEffort(Math.max(numEffort - 1, 0))}> -
27 |
{ratingRenderer("@", numEffort)}
28 |
setNumEffort(Math.min(numEffort + 1, 5))}> +
29 |
30 |
31 |
32 | 33 |
34 |
setNumIntimacy(Math.max(numIntimacy - 1, 0))}> -
35 |
{ratingRenderer("#", numIntimacy)}
36 |
setNumIntimacy(Math.min(numIntimacy + 1, 5))}> +
37 |
38 |
39 |
40 |
onComplete()}>close
41 |
postRating(exercise.id, numEffort, numIntimacy, onComplete)}>submit
42 |
43 |
44 | 45 | 46 |
47 | 48 | ) 49 | } 50 | 51 | export default CardReflection; 52 | -------------------------------------------------------------------------------- /components/Layouts/Header.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | import { LOGO_FLICKER_TIME } from "../../data/times"; 6 | 7 | const Header: React.FC = () => { 8 | 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |
17 | protocols for remote connection 18 |
19 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | const ActiveLink : React.FC<{href: string, text: string}> = ({href, text}) => { 32 | const { asPath } = useRouter() 33 | 34 | return ( 35 | 36 | {text} 37 | 38 | ) 39 | } 40 | 41 | const Logo: React.FC = () => { 42 | 43 | let [one, setOne] = useState("1"); 44 | 45 | const flicker = useCallback(() => { 46 | if (Math.random() > 0.5) { 47 | setOne("_"); 48 | setTimeout(() => setOne("1"), 300); 49 | } 50 | }, [setOne]); 51 | 52 | useEffect(() => { 53 | flicker(); 54 | let interval = setInterval(flicker, LOGO_FLICKER_TIME); 55 | return () => clearInterval(interval); 56 | }, [flicker]) 57 | 58 | return ( 59 | 60 | 1:{one} 61 | 62 | ) 63 | } 64 | 65 | export default Header; 66 | -------------------------------------------------------------------------------- /components/Layouts/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { useRouter } from "next/router"; 3 | import BackgroundProvider from "../BackgroundProvider"; 4 | import Header from "./Header"; 5 | import React from "react"; 6 | 7 | 8 | const Layout: React.FC<{ pageName?: string }> = ({ children, pageName }) => { 9 | 10 | const router = useRouter(); 11 | return ( 12 |
13 | 14 | {pageName || "protocols for remote connection"} 15 | 16 | 17 | 18 | 22 | 23 | 24 |
25 |
26 | {children} 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Layout; 33 | -------------------------------------------------------------------------------- /components/PageInternal/Oracle.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 3 | import { getRandomExercise } from "../../data/protocols"; 4 | import { useSetBackgroundID } from "../BackgroundProvider"; 5 | import { OracleCompletedText, OraclePromptActionText, OraclePromptText, OracleUpdatingText } from "../../data/prompts"; 6 | import { BG_TRANSITION_TIME, PER_ORACLE_TEXT_TIME } from "../../data/times"; 7 | import { PromptDialog, UpdatingOracleText } from "./TextLayouts"; 8 | 9 | const Oracle: React.FC = () => { 10 | const [status, setStatus] = useState<"SUMMONED" | "INITIAL" | "COMPLETE">("INITIAL"); 11 | const router = useRouter(); 12 | 13 | const displayOracle = useCallback(() => { 14 | console.log("RUNNING ORACLE", status); 15 | switch (status) { 16 | case "SUMMONED": 17 | setStatus("COMPLETE"); 18 | let exercise = getRandomExercise(); 19 | router.push("/" + exercise.id); 20 | return ... ; 21 | case "INITIAL": 22 | return ( 23 |
24 |
25 | setStatus("SUMMONED")} /> 26 |
27 |
28 | ); 29 | default: 30 | case "COMPLETE": 31 | return null; 32 | } 33 | }, [status, router]); 34 | 35 | return displayOracle(); 36 | }; 37 | 38 | const OracleAnimation: React.FC<{ onPromptComplete: () => void }> = ({ onPromptComplete }) => { 39 | const [stage, setStage] = useState(0); 40 | const setBGID = useSetBackgroundID(); 41 | 42 | useEffect(() => { 43 | // console.log("stage changed", stage); 44 | switch (stage) { 45 | case 0: 46 | setBGID("home"); 47 | break; 48 | case 0.25: 49 | case 0.5: 50 | setBGID("stars"); 51 | break; 52 | case 1: 53 | setBGID("dots"); 54 | break; 55 | case 2: 56 | setBGID("galaxy0"); 57 | setTimeout(() => setBGID("galaxy1"), PER_ORACLE_TEXT_TIME); 58 | setTimeout(() => setBGID("galaxy2"), 2 * PER_ORACLE_TEXT_TIME); 59 | break; 60 | case 3: 61 | setBGID("dots"); 62 | break; 63 | case 4: 64 | onPromptComplete(); 65 | break; 66 | default: 67 | setBGID("blank"); 68 | break; 69 | } 70 | }, [stage, setBGID, onPromptComplete]); 71 | 72 | const displayText = useMemo((): JSX.Element | null => { 73 | switch (stage) { 74 | case 0: { 75 | return ( 76 | setStage(0.25)} 78 | promptText={OraclePromptActionText[0]} 79 | textStrings={OraclePromptText[0]} 80 | /> 81 | ); 82 | } 83 | case 0.25: { 84 | return ( 85 | setStage(0.5)} 87 | promptText={OraclePromptActionText[1]} 88 | textStrings={OraclePromptText[1]} 89 | /> 90 | ); 91 | } 92 | case 0.5: { 93 | return ( 94 | setStage(1)} 96 | promptText={OraclePromptActionText[2]} 97 | textStrings={OraclePromptText[2]} 98 | /> 99 | ); 100 | } 101 | case 1: 102 | return ( 103 | setStage(2)} textStrings={OracleUpdatingText} key="dots" /> 104 | ); 105 | case 2: 106 | return ( 107 | setStage(3)} 109 | textStrings={[[""]]} 110 | key="galaxy" 111 | timePerStage={3 * PER_ORACLE_TEXT_TIME} 112 | /> 113 | ); 114 | case 3: 115 | return ( 116 | setStage(4)} 118 | textStrings={OracleCompletedText} 119 | key="complete" 120 | /> 121 | ); 122 | default: 123 | return null; 124 | } 125 | }, [stage]); 126 | 127 | return displayText; 128 | }; 129 | 130 | export default Oracle; 131 | -------------------------------------------------------------------------------- /components/PageInternal/Protocol.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { getExerciseByID } from "../../data/protocols"; 3 | import CardDetail from "../Card/CardDetail"; 4 | 5 | const ProtocolPageInternal: React.FC = () => { 6 | const { query, isReady } = useRouter() 7 | 8 | const eid = query.id as string 9 | const exercise = getExerciseByID(eid as string); 10 | return ( 11 |
12 | {!isReady ? ( 13 |
Loading
14 | ) : exercise ? ( 15 | 16 | ) : ( 17 |
Exercise not found
18 | )} 19 |
20 | ); 21 | }; 22 | 23 | export default ProtocolPageInternal; 24 | -------------------------------------------------------------------------------- /components/PageInternal/TextLayouts.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { PER_ORACLE_TEXT_TIME } from "../../data/times"; 3 | import { textToP } from "../../lib/textTransform"; 4 | 5 | 6 | interface PromptDialogProps { 7 | textStrings: string[]; 8 | onPromptClicked: () => void; 9 | promptText: string; 10 | } 11 | export const PromptDialog = ({ 12 | textStrings, 13 | onPromptClicked, 14 | promptText, 15 | }: PromptDialogProps) => { 16 | return ( 17 |
18 | {textToP(textStrings)} 19 | 20 | {promptText} 21 | 22 |
23 | ); 24 | }; 25 | 26 | export const ratingRenderer = (symbol: string, n: number) => { 27 | let stars = []; 28 | for (let i = 0; i < n; i++) { 29 | stars.push(symbol); 30 | } 31 | return stars; 32 | }; 33 | 34 | export const UpdatingOracleText: React.FC<{ 35 | textStrings: string[][]; 36 | onAnimationComplete: () => void; 37 | timePerStage?: number; 38 | }> = ({ textStrings, onAnimationComplete, timePerStage = PER_ORACLE_TEXT_TIME }) => { 39 | let [textStage, setTextStage] = useState(0); 40 | let [activeText, setActiveText] = useState([]); 41 | 42 | useEffect(() => { 43 | // console.log("text stage", textStage, textStrings); 44 | 45 | if (textStage >= textStrings.length) { 46 | onAnimationComplete(); 47 | return; 48 | } 49 | 50 | setActiveText(textStrings[textStage]); 51 | }, [textStage, textStrings, onAnimationComplete]); 52 | 53 | useEffect(() => { 54 | let interval = setInterval(() => setTextStage((t) => t + 1), timePerStage); 55 | return () => clearInterval(interval); 56 | }, [timePerStage]); 57 | 58 | return
{textToP(activeText)}
; 59 | }; 60 | -------------------------------------------------------------------------------- /components/types.d.ts: -------------------------------------------------------------------------------- 1 | type Suit = "chime" | "weave" | "astral" | "flow"; 2 | interface Rating { 3 | intimacy: number; 4 | effort: number; 5 | } 6 | 7 | interface Exercise { 8 | id: string, 9 | name: string, 10 | text: string, 11 | rating: Rating, 12 | suit: Suit, 13 | index: number 14 | } 15 | 16 | interface exerciseProps { 17 | exercise: Exercise; 18 | } 19 | -------------------------------------------------------------------------------- /data/backgrounds.ts: -------------------------------------------------------------------------------- 1 | import { padString } from "../lib/textTransform"; 2 | 3 | let bgGalaxy1 = ` 4 | . . . , . . . 5 | . . . . . }. . . . “ . . 6 | ~ . . / :. 7 | . * . . {: ; . . . . 8 | “+_;’ 9 | . . . . . * . . . 10 | . . . . _ . . * . 11 | . . ~ . . .*. . ….. 12 | . . . . .._;: .: ""- . - . . . 13 | . . ..: ...-:;' :: . .. .: ' ;.’ 14 | . ::: .' .:':':: -' . ..~ * * ’ ):. '. 15 | . _. . ..: :' :~ _ ; . .~;* : :/ . .:' 16 | . ':: . ~~. : . .. * : .:'' .-' 17 | . . . ' + ''' .. ..:’ .;;-' . . 18 | . . "" ... .. ....--' .. :’ . . 19 | . . . " " . . “ . . . . 20 | . .. . . . . . . ..:’. 21 | “ 22 | . . . . . . . . . 23 | . . . * . 24 | 25 | . . . . . 26 | 27 | ` 28 | 29 | let bgGalaxy2 = ` 30 | . . . . . . 31 | . . . . . . .. 32 | . . . . . . 33 | . . ..:’. * . . . . “ . . 34 | . . ~ . .. . .. ….. 35 | . . . . .. . " "- - . . . 36 | . ....:’ __....--' ...:':: ::%:.:: '-. 37 | .::” - ' ..::''''' ::: _--':+;_:. '. . 38 | . “ .' . ..:' .. .. . : ):. '. 39 | . ..: :' _ S LE I G RO O L : ; . .:' ; 40 | . ':: .. : *:: .:'' .-' 41 | . '~ . ':. : ''' . .. _.-' . . 42 | . . . ' + ''' .. . .-' . .:. . 43 | . . "" ... .. ....--' . . ;::’” . 44 | . . . " " . . ._++”” . . 45 | . .. . . . .::;;”” . . 46 | “ . . . :’ 47 | . . . . . . . . . . . 48 | . . . . . 49 | . . . . . 50 | 51 | 52 | ` 53 | 54 | 55 | let bgGalaxy3 = ` 56 | . . . _......____._ . . 57 | . . . ..--'"" . """"""---... . . 58 | …. ...:':: :::%:.::::::_; '-. 59 | .- ' ..::::''''' .:.::: _--'"""":::+;_: :. '. . 60 | . .' . ..:' _.-.. .. . :::)::. '. 61 | . ..;: :' _.-' .. . . f::':: . 62 | . .' ::'.' .-" .:” .. '-' ::%::. ‘ . 63 | .: .;:’ / SELECTING PROTOCOL... :::::’ . 64 | | :.%:' . : (' ' .' .-" - ::.:: . ‘ 65 | . ::: ( -..____...-' .-" .::' . 66 | ':': '. : ..--' . .:':: . 67 | '::: '-._____...- .. ..::: ::%:: .:::'' .-' 68 | . '. . '::::%::: :::: ''' _.-' . 69 | '-.. . . '''''' . .-' . . 70 | . ""--...____ . ______......--' . . . 71 | . . """""""" . . . . . 72 | . . . . 73 | . . . . 74 | ` 75 | 76 | 77 | 78 | let bgDots = ` 79 | . . . . . 80 | 81 | . . . . . “ . 82 | 83 | . " . . . . 84 | . . . . . 85 | 86 | . . . “ . 87 | 88 | . . 89 | . 90 | 91 | . . 92 | 93 | . . . 94 | 95 | . . 96 | 97 | . . . . 98 | 99 | . " . . . . 100 | 101 | . . . . . 102 | 103 | . . . . . “ . 104 | 105 | . " . . . . 106 | 107 | ` 108 | 109 | const bgHome = ` 110 | 111 | 112 | 113 | 114 | 115 | 116 | " 117 | 118 | # 119 | " __________ 120 | |’. .’| 121 | | .\\ __ /. | 122 | |/________\\| 123 | 124 | .*. 125 | *** 126 | V 127 | /\\|/\\ 128 | | 129 | | 130 | . 131 | 132 | 133 | 134 | 135 | 136 | . 137 | ._______. = ____ 138 | | ~~~~~ | // ||""|| 139 | \\ ~~~~~ \\ !/ " ||__|| 140 | .| ~~~~~ | * [ -=.] 141 | {}.......| ====== 142 | 143 | <3 144 | <3 145 | ` 146 | 147 | 148 | 149 | 150 | const bgMapping: { [key: string]: string } = { 151 | blank: padString(' ', ["."], 0.997), 152 | galaxy0: padString(bgGalaxy1, ["."], 0.96), 153 | galaxy1: padString(bgGalaxy2, ["."], 0.96), 154 | galaxy2: padString(bgGalaxy3, ["."], 0.96), 155 | dots: padString(" ", [".", ","], 0.99), 156 | stars: padString(" ", [".", "*"], 0.99), 157 | home: padString(bgHome, ["."]) 158 | }; 159 | 160 | 161 | 162 | export default bgMapping; 163 | -------------------------------------------------------------------------------- /data/prompts.ts: -------------------------------------------------------------------------------- 1 | export const OraclePromptText = [ 2 | ["missing someone from afar?", "consult this connection wizard for how best to reach them"], 3 | [ 4 | "what does it mean to connect from afar in our present?", 5 | "as digital fatigue lingers and physical happenings spring back,what happens to remote connectivity?", 6 | ], 7 | [ 8 | "if you are missing a faraway peer, the wizard of 1:1 is here to offer a bit of guidance", 9 | "digital or analog, synchronous or asynchronous the wizard will select a promising protocol for you", 10 | "this will be an exercise for you and your peer to perform in order to experiment with remote connection together", 11 | " ", 12 | " ", 13 | "ready?", 14 | ], 15 | ]; 16 | 17 | export const OraclePromptActionText = [ 18 | "request a protocol", 19 | ">", 20 | "begin" 21 | ] 22 | 23 | export const OracleUpdatingText = [ 24 | ["please bring your subject to mind"], 25 | ["what makes them who they are? "], 26 | ["tune into their characteristics and traits"], 27 | ["rest one hand on your heart and the other on this screen"], 28 | ]; 29 | 30 | export const OracleCompletedText = [["PROTOCOL SELECTED"]]; 31 | 32 | export const CardIntroText = ["the wizard has selected the following protocol for you and your peer to perform. "]; 33 | 34 | 35 | 36 | export const ReflectionText = [ 37 | "Reflect on the protocol", 38 | "Each protocol demands something different from its peers. Some ask for dedicated time and space, while others are quick and easy to perform. In return the protocol can give you suprising gifts ranging from feelings of deep intimacy to the gesture of a quick check in. ", 39 | "Once you have completed the protocol with a peer, we ask that you rate its Intimacy (#) and Effort (@). These reflections are stored anonymously and the average ratings are displayed on this site.", 40 | ]; 41 | -------------------------------------------------------------------------------- /data/protocols.ts: -------------------------------------------------------------------------------- 1 | const exercises: Exercise[] = [ 2 | { 3 | text: "Share 3-5 songs that you are listening to today.", 4 | name: "Mixtape", 5 | id: "mixtape", 6 | suit: "chime", 7 | rating: { intimacy: 3, effort: 2 }, 8 | index: 1 9 | }, 10 | { 11 | text: "I. Find a device that records sound \nII. Move the device along your body from head to toe\nIII. Let each body part 'speak for itself' along the way\nIV. Send the recordings to each other ", 12 | name: "Audible Body Scan", 13 | id: "audible-body-scan", 14 | suit: "astral", 15 | rating: { intimacy: 3, effort: 5 }, 16 | index: 2 17 | }, 18 | { 19 | text: "I. As you go about your day, notice if something reminds you of your peer\nII. Make and share a record of it", 20 | name: "Forget Me Not", 21 | id: "forget-me-not", 22 | suit: "astral", 23 | rating: { intimacy: 4, effort: 1 }, 24 | index: 3 25 | }, 26 | { 27 | text: "I. Get on a voice call and go outside together.\nII. Narrate your surroundings out loud. It's okay if your voices overlap or cut into each other.\nIII. Notice how your attention shifts between the two places.", 28 | name: "Neighborhood Tour", 29 | id: "neighborhood-tour", 30 | suit: "weave", 31 | rating: { intimacy: 4, effort: 5 }, 32 | index: 4 33 | }, 34 | { 35 | text: "Leave each other a few voice messages across the different digital channels where you are both connected.", 36 | name: "Scavenger Hunt", 37 | id: "scavenger-hunt", 38 | suit: "chime", 39 | rating: { intimacy: 4, effort: 4 }, 40 | index: 5 41 | }, 42 | { 43 | text: "Set up an video call and then continue do whatever you were doing (no need to acknowledge each other).", 44 | name: "Parallel Play", 45 | id: "parallel-play", 46 | suit: "flow", 47 | rating: { intimacy: 4, effort: 5 }, 48 | index: 6 49 | }, 50 | { 51 | text: "Check in on each other's friends", 52 | name: "Takes a Village", 53 | id: "takes-a-village", 54 | suit: "weave", 55 | rating: { intimacy: 5, effort: 5 }, 56 | index: 7 57 | }, 58 | { 59 | text: "Work on a project together", 60 | name: "Collaborate", 61 | id: "collaborate", 62 | suit: "weave", 63 | rating: { intimacy: 5, effort: 5 }, 64 | index: 8 65 | }, 66 | { 67 | text: "I. Come up with a few questions\nII. Discuss by co-writing on an Etherpad (or equivalent)", 68 | name: "For the Record", 69 | id: "for-the-record", 70 | suit: "flow", 71 | rating: { intimacy: 3, effort: 5 }, 72 | index: 9 73 | }, 74 | { 75 | text: "Draw the network diagram between you two.", 76 | name: "Birds Eye View", 77 | id: "birds-eye-view", 78 | suit: "weave", 79 | rating: { intimacy: 3, effort: 5 }, 80 | index: 10 81 | }, 82 | { 83 | text: "Send a notification every time you are experiencing a heightened emotion today.", 84 | name: "I'm Feeling...", 85 | id: "im-feeling", 86 | suit: "chime", 87 | rating: { intimacy: 3, effort: 3 }, 88 | index: 11 89 | }, 90 | { 91 | text: "Share increasingly hype images with each other, until the most hype image is found.", 92 | name: "Hype or hyper", 93 | id: "hype-or-hyper", 94 | suit: "flow", 95 | rating: { intimacy: 2, effort: 4 }, 96 | index: 12 97 | }, 98 | { 99 | text: "Find an inanimate object that reminds you of your peer, and use it as a phone to talk to them.", 100 | name: "Ghost Call", 101 | id: "ghost-call", 102 | suit: "astral", 103 | rating: { intimacy: 5, effort: 1 }, 104 | index: 13 105 | }, 106 | { 107 | text: "I. Close your eyes\nII. Make an intuitive movement or gesture with your whole body \nIII. Sketch it on a piece of paper or a postcard\nIV. Send it in the mail (ask for an address if needed)", 108 | name: "Paper Pose", 109 | id: "paper-pose", 110 | suit: "flow", 111 | rating: { intimacy: 5, effort: 5 }, 112 | index: 14 113 | }, 114 | { 115 | text: "I. Calculate the angle between your cities\nII. Use a compass to walk 10 minutes in that direction\nIII. Share the map pin and a photo of where you end up", 116 | name: "On My Way", 117 | id: "on-my-way", 118 | suit: "weave", 119 | rating: { intimacy: 5, effort: 4 }, 120 | index: 15 121 | }, 122 | { 123 | text: "Make an e-card to say good morning", 124 | name: "Greeting Card", 125 | id: "greeting-card", 126 | suit: "chime", 127 | rating: { intimacy: 3, effort: 4 }, 128 | index: 16 129 | }, 130 | { 131 | text: "I. Take a photo or make a sketch of where you are right now\nII. Annotate it with all the sounds you hear around you", 132 | name: "Soundscape", 133 | id: "soundscape", 134 | suit: "astral", 135 | rating: { intimacy: 4, effort: 4 }, 136 | index: 17 137 | }, 138 | { 139 | text: "Share the first link you looked at today", 140 | name: "News Feed", 141 | id: "news-feed", 142 | suit: "flow", 143 | rating: { intimacy: 2, effort: 1 }, 144 | index: 18 145 | }, 146 | ]; 147 | 148 | export const getAllExercises = () => { 149 | return exercises; 150 | }; 151 | 152 | export const getRandomExercise = () => { 153 | return exercises[Math.floor(Math.random() * exercises.length)]; 154 | }; 155 | 156 | export const getExerciseByID = (id: string) => { 157 | console.log("Searching for", id); 158 | return exercises.find((exercise) => exercise.id == id); 159 | }; 160 | -------------------------------------------------------------------------------- /data/server.ts: -------------------------------------------------------------------------------- 1 | 2 | const serverURL = `https://remote-protocol-server.glitch.me/`; 3 | 4 | export const getRatings = (id: string) => { 5 | return fetch(`${serverURL}/protocol?id=${id}`); 6 | } 7 | 8 | export const postRating = (id: string, effort: number, intimacy: number, callback: () => void) => { 9 | const request = JSON.stringify({ name: id, effort: effort, intimacy: intimacy }); 10 | console.log("Sending", request); 11 | const requestOptions = { 12 | method: "POST", 13 | headers: { "Content-Type": "application/json" }, 14 | body: request, 15 | }; 16 | fetch("https://remote-protocol-server.glitch.me/protocol", requestOptions) 17 | .then(() => { 18 | console.log("sucessfull post"); 19 | callback(); 20 | }) 21 | .catch((e) => { 22 | console.log(e); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /data/suitIllustrations.ts: -------------------------------------------------------------------------------- 1 | import { whitespaceToDots } from "../lib/textTransform"; 2 | 3 | const chimeBG = ` 4 | 8 5 | . 6 | @ 7 | * 8 | C | 9 | . 10 | H . 11 | . 12 | I 13 | . M 14 | . . . . . . . . 15 | . E 16 | . 17 | . 18 | ! 19 | . 20 | . 21 | | 22 | * 23 | @ 24 | . 25 | ` 26 | 27 | const astralBG = ` 28 | ‘ 29 | ‘ 30 | ' 31 | , * 32 | % , 33 | l 34 | 35 | a 36 | ~ 37 | R 38 | 39 | * 40 | T 41 | * 42 | S 43 | A 44 | ? 45 | , 46 | , 47 | , 48 | , 49 | ` 50 | 51 | const weaveBG = ` 52 | ⌘ ⌘ ⌘ 53 | ⌘ ⌘ 54 | ⌘ ⌘ ⌘ 55 | ⌘ 56 | ⌘ ⌘ ⌘ 57 | ⌘ ⌘ 58 | ⌘ ⌘ 59 | ⌘ ⌘ ⌘ 60 | ⌘ 61 | ⌘ 62 | ⌘ ⌘ 63 | 64 | e 65 | A v 66 | W 67 | E 68 | ⌘ 69 | ⌘ 70 | ⌘ ⌘ ⌘ 71 | ⌘ ⌘ ⌘ 72 | ⌘ ⌘ ⌘ ⌘ 73 | ` 74 | 75 | const flowBG = ` 76 | +=+ . 77 | =+=+=+ ; + 78 | +=+= . 79 | =+= . . 80 | +=+ . =+= 81 | =+= +..; 82 | +=+ .+. 83 | = ; 84 | + . 85 | = + . 86 | + f +=+= 87 | = +=+ 88 | + l += 89 | =+ + + 90 | =+= ; 91 | + + 0 . 92 | = = 93 | =+ + 94 | += . W 95 | =+= + ; 96 | +=+= ; .. ` 97 | 98 | 99 | const suitMapping: Record = { 100 | chime: chimeBG, 101 | weave: weaveBG, 102 | astral: astralBG, 103 | flow: flowBG, 104 | } 105 | 106 | export default suitMapping; -------------------------------------------------------------------------------- /data/times.ts: -------------------------------------------------------------------------------- 1 | export const BG_TRANSITION_TIME = 3000; 2 | export const BG_NUM_STAGES = 30; 3 | export const PER_ORACLE_TEXT_TIME = 6000; 4 | export const LOGO_FLICKER_TIME = 2000; 5 | -------------------------------------------------------------------------------- /lib/textTransform.tsx: -------------------------------------------------------------------------------- 1 | export const BGHEIGHT = 44; 2 | export const BGWIDTH = 120; 3 | export const RAND_DENSITY = 0.99; 4 | 5 | 6 | /* Rendering */ 7 | 8 | export const textToP = (text: string[]) => { 9 | return ( 10 |
11 | {text.map((t, i) => ( 12 |

{t}

13 | ))} 14 |
15 | ); 16 | } 17 | 18 | 19 | export const AsciiRender = ({text}: {text:string}) => { 20 | return ( 21 |
22 |       
23 |         {text}
24 |       
25 |     
26 | ) 27 | } 28 | 29 | /* Background manipulation */ 30 | 31 | function getRandomCharacter(randomChars?: string[], density: number = RAND_DENSITY) { 32 | if (randomChars && Math.random() > density) { 33 | return randomChars[Math.floor(Math.random() * randomChars.length)]; 34 | } else { 35 | return " "; 36 | } 37 | } 38 | 39 | export function padString(s: string, randomChars?: string[], density?: number) { 40 | let eachLine = s.split("\n"); 41 | let numLines = eachLine.length; 42 | 43 | //Add extra lines 44 | if (numLines < BGHEIGHT) { 45 | let numPadding = BGHEIGHT - numLines; 46 | let numEachSide = Math.floor(numPadding / 2); 47 | for (let i =0; i< numEachSide; i++) { 48 | eachLine.push(" "); 49 | eachLine.unshift(" "); 50 | } 51 | 52 | } 53 | //For each line, pad sides 54 | for (let i=0; i < eachLine.length; i++) { 55 | let line = eachLine[i]; 56 | let pad = Math.floor((BGWIDTH - line.length) / 2); 57 | let leftPad = ""; 58 | let rightPad = ""; 59 | for (let j=0; j < pad; j++) { 60 | leftPad += getRandomCharacter(randomChars, density); 61 | rightPad += getRandomCharacter(randomChars, density); 62 | } 63 | eachLine[i] = leftPad + line + rightPad; 64 | } 65 | 66 | let paddedString = eachLine.join("\n"); 67 | return paddedString 68 | } 69 | 70 | 71 | 72 | export function whitespaceToDots(text: string, char: string = ".") { 73 | 74 | 75 | let lines = text.split("\n"); 76 | 77 | for (let i =0 ; i< lines.length; i++){ 78 | let line = lines[i]; 79 | let newLine = ""; 80 | for (let j =0; j < line.length; j++) { 81 | let c = line[j]; 82 | if (c === " ") { 83 | newLine += char; 84 | } else { 85 | newLine += c; 86 | } 87 | } 88 | lines[i] = newLine; 89 | } 90 | return lines.join("\n"); 91 | } -------------------------------------------------------------------------------- /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 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercises_for_two", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "export": "next build && next export" 11 | }, 12 | "dependencies": { 13 | "classnames": "^2.3.1", 14 | "copy-to-clipboard": "^3.3.1", 15 | "next": "12.1.0", 16 | "react": "17.0.2", 17 | "react-dom": "17.0.2", 18 | "react-spring": "^9.4.5", 19 | "react-visibility-sensor": "^5.1.1", 20 | "slugify": "^1.6.5" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "17.0.21", 24 | "@types/react": "17.0.40", 25 | "eslint": "8.11.0", 26 | "eslint-config-next": "12.1.0", 27 | "typescript": "4.6.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pages/[id].tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import ProtocolPageInternal from "../components/PageInternal/Protocol"; 3 | import Layout from "../components/Layouts/Layout"; 4 | 5 | const ProtocolPage: NextPage = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default ProtocolPage; 14 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "../styles/tarotCard.css"; 3 | import type { AppProps } from "next/app"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default MyApp; 12 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Image from "next/image"; 4 | import Header from "../components/Layouts/Header"; 5 | import Layout from "../components/Layouts/Layout"; 6 | import Oracle from "../components/PageInternal/Oracle"; 7 | import { getRandomExercise } from "../data/protocols"; 8 | 9 | const Home: NextPage = () => { 10 | return ( 11 | 12 |
13 |
14 |
15 |

Your friend is far away, and you miss them. How can you reach them?

16 | 17 |

18 | It often feels like we are stuck with a few dominant digital platforms that promise us connection but leave us feeling sedated and lost. We might start to send a message and get trapped in the sticky mess of feeds and ads. We crave intimacy and are encouraged to meet that need by sharing content to get more likes. 19 |

20 | 21 |

22 | 1:1 is a set of remote connection protocols to help you reach someone from a distance. Ground yourself with intention and attention and ask the wizard to select a protocol. Share this protocol with your remote peer and follow its instructions to experiment together. Reflect on how it feels to share signals this way, and add your own protocol ideas to the collection. What kinds of protocols may bring us closer with each other? Would they move us to transmit a small gift, a moment of presence, a routine meditation, a sweet something? 23 |

24 | 25 |

26 | This project began when two friends far apart decided to send a signal to each other every day for two weeks. We improvised with different ways to transmit this signal and observed whether they made us feel closer as well as the amount of effort it took to send each. 27 |

28 |
29 |
30 |

a note on accessibility:

31 | 32 |

33 | the list of possible protocols ranges across digital and analog, synchronous and asynchronous, and various 34 | body senses. depending on the devices, bandwidth, disabilities, location across time and space, or other 35 | circumstances that you and your partner have, not all protocols may be applicable. 36 |

37 | 38 |

feel free adapt the instructions as you prefer, or ask the wizard again

39 |
40 |
41 |

Bios

42 | 43 |

44 | Alice Yuan Zhang 张元 (she/her) is a Chinese-American media artist and educator. Her practice operates on 45 | cyclical and intergenerational time. Along the peripheries of imperialist imagination, she works to bridge 46 | ecology and technology through ancestral remembering, interspecies pedagogy, and translocal solidarity. 47 |
48 | 49 | Instagram 50 | {" "} 51 | |{" "} 52 | 53 | Twitter 54 | {" "} 55 | |{" "} 56 | 57 | Mastodon 58 | {" "} 59 | |{" "} 60 | 61 | Website 62 | 63 |

64 | 65 |

soft networks is an exploratory design and development studio. We create slow, intimate, tools as a means of imagining alternate futures for social software. 66 |
67 | 68 | Website 69 | {" "} 70 | |{" "} 71 | 72 | Instagram 73 | {" "} 74 | |{" "} 75 | 76 | Twitter 77 | 78 |

79 |
80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Home; 87 | -------------------------------------------------------------------------------- /pages/all.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useEffect } from "react"; 3 | import { useSetBackgroundID } from "../components/BackgroundProvider"; 4 | import CardPreview from "../components/Card/CardPreview"; 5 | import Layout from "../components/Layouts/Layout"; 6 | import { getAllExercises } from "../data/protocols"; 7 | 8 | const All: NextPage = () => { 9 | const setBGID = useSetBackgroundID(); 10 | 11 | useEffect(() => { 12 | setBGID("dots"); 13 | }, [setBGID]); 14 | 15 | return ( 16 | 17 |
18 |
19 |

20 | scroll down to review all the protocols we have collected so far. to submit your own protocol,{" "} 21 | 26 | send us an email. 27 | 28 |

29 |
30 |
31 | {getAllExercises().map((e, i) => ( 32 | 33 | ))} 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default All; 41 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import bgRender from "../components/BackgroundProvider"; 3 | import Layout from "../components/Layouts/Layout"; 4 | import Oracle from "../components/PageInternal/Oracle"; 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Home; 15 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soft-networks/remote-protocols/76b4615c447238e594a363a5659baa1d46c3b8ed/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ratio: 1.4; 3 | --s0: 1em; 4 | --s1: calc(var(--s0) * var(--ratio)); 5 | --s2: calc(var(--s1) * var(--ratio)); 6 | --s3: calc(var(--s2) * var(--ratio)); 7 | --s4: calc(var(--s3) * var(--ratio)); 8 | --s5: calc(var(--s4) * var(--ratio)); 9 | --s-1: calc(var(--s0) / var(--ratio)); 10 | --s-2: calc(var(--s-1) / var(--ratio)); 11 | --s-3: calc(var(--s-2) / var(--ratio)); 12 | --s-4: calc(var(--s-3) / var(--ratio)); 13 | --s-5: calc(var(--s-4) / var(--ratio)); 14 | 15 | --spacing: var(--s0); 16 | --stackSpacing: var(--s0); 17 | 18 | --gray: #10163b; 19 | --light: #dcf9ff; 20 | --black: #0a0f2a; 21 | --white: white; 22 | --contrast: pink; 23 | 24 | --textWidth: 48ch; 25 | 26 | } 27 | 28 | * { 29 | box-sizing: border-box; 30 | padding: 0; 31 | margin: 0; 32 | } 33 | 34 | html, 35 | body, 36 | main, 37 | #__next { 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | body { 43 | background: var(--gray); 44 | line-height: 1.2; 45 | color: var(--light); 46 | font-family: 'Share Tech Mono', monospace; 47 | 48 | } 49 | 50 | 51 | 52 | /* Sizing */ 53 | .fullBleed { 54 | width: 100%; 55 | height: 100%; 56 | position: relative; 57 | } 58 | 59 | .fullHeight { 60 | height: 100%; 61 | } 62 | 63 | .fullWidth { 64 | width: 100%; 65 | } 66 | 67 | 68 | .maxFullScreen { 69 | max-width: 100%; 70 | max-height: 100%; 71 | } 72 | 73 | 74 | 75 | p { 76 | max-inline-size: var(--textWidth); 77 | } 78 | 79 | .wide { 80 | --textWidth: 100%; 81 | } 82 | 83 | .narrow { 84 | --textWidth: 32ch; 85 | } 86 | 87 | .medium { 88 | --textWidth: 72ch; 89 | } 90 | 91 | .padded { 92 | padding: var(--spacing); 93 | } 94 | 95 | .padded\:s1 { 96 | padding: var(--s1); 97 | } 98 | 99 | .padded\:s2 { 100 | padding: var(--s2); 101 | } 102 | .padded\:s3 { 103 | padding-top: var(--s3); 104 | } 105 | 106 | .padded\:s-1 { 107 | padding: var(--s-1); 108 | } 109 | 110 | 111 | 112 | /* layout */ 113 | 114 | .relative { 115 | position: relative; 116 | } 117 | 118 | .noOverflow { 119 | overflow: hidden; 120 | } 121 | 122 | .noOverflowX { 123 | overflow: hidden; 124 | } 125 | 126 | .hide { 127 | display: none; 128 | } 129 | 130 | .centerh { 131 | max-inline-size: var(--textWidth); 132 | margin-inline: auto; 133 | } 134 | 135 | .halfWidth { 136 | max-inline-size: min(80%, 1200px); 137 | } 138 | 139 | .quarterWidth { 140 | max-inline-size: min(42%, 600px); 141 | } 142 | 143 | .cover { 144 | display: flex; 145 | flex-direction: column; 146 | min-block-size: 100%; 147 | } 148 | 149 | .cover > .centerv { 150 | margin-block: auto ; 151 | } 152 | 153 | 154 | .center\:absolute { 155 | position: absolute; 156 | top: 50%; 157 | left: 50%; 158 | 159 | transform: translate(-50%, -50%); 160 | z-index: -1; 161 | overflow: hidden; 162 | } 163 | .higher { 164 | z-index: 1 165 | } 166 | 167 | 168 | .align\:center { 169 | display: flex; 170 | justify-content: center; 171 | } 172 | .align-start\:vertical { 173 | display: flex; 174 | align-items: flex-start; 175 | } 176 | 177 | .center-text { 178 | text-align: center; 179 | } 180 | 181 | .grid { 182 | display: flex; 183 | flex-wrap: wrap; 184 | gap: var(--s0); 185 | justify-content: center; 186 | } 187 | 188 | .flex-1 { 189 | flex: 1 190 | } 191 | 192 | 193 | 194 | 195 | 196 | .stack, 197 | .stack\:noGap, 198 | .stack\:s-1, 199 | .stack\:s1, 200 | .stack\:s2, 201 | .stack\:custom { 202 | display: flex; 203 | flex-direction: column; 204 | --stackSpacing: var(--s0); 205 | } 206 | 207 | .stack > * + * { 208 | margin-block-start: var(--s0); 209 | } 210 | 211 | .stack\:custom > * + * { 212 | margin-block-start: var(--stackSpacing); 213 | } 214 | 215 | .stack\:noGap>*+* { 216 | margin-block-start: 0; 217 | } 218 | 219 | .stack\:s-1>*+* { 220 | margin-block-start: var(--s-1); 221 | } 222 | 223 | .stack\:s1>*+* { 224 | margin-block-start: var(--s1); 225 | } 226 | 227 | .stack\:s2>*+* { 228 | margin-block-start: var(--s2); 229 | } 230 | 231 | .horizontal-stack { 232 | display: flex; 233 | flex-direction: row; 234 | align-items: flex-start; 235 | } 236 | 237 | 238 | .horizontal-stack>*+* { 239 | margin-left: var(--s0); 240 | } 241 | 242 | .stack .align-end { 243 | margin-top: auto; 244 | } 245 | 246 | 247 | .horizontal-stack .align-end { 248 | margin-left: auto; 249 | } 250 | 251 | 252 | 253 | /* aesthetics + interactivity */ 254 | 255 | code, 256 | pre { 257 | line-height: 1.1; 258 | font-size: inherit; 259 | font-family: 'Share Tech Mono', monospace; 260 | } 261 | 262 | 263 | .border { 264 | border: 1px solid var(--light); 265 | 266 | } 267 | 268 | a { 269 | color: inherit; 270 | text-decoration: none; 271 | } 272 | 273 | .active a { 274 | text-decoration: underline; 275 | } 276 | 277 | a:hover, 278 | .button:hover { 279 | color: var(--light); 280 | cursor: pointer; 281 | } 282 | 283 | .noSelect { 284 | -webkit-touch-callout: none; 285 | -webkit-user-select: none; 286 | -khtml-user-select: none; 287 | -moz-user-select: none; 288 | -ms-user-select: none; 289 | user-select: none; 290 | } 291 | 292 | .button { 293 | text-transform: uppercase; 294 | } 295 | .uppercase { 296 | text-transform: uppercase; 297 | } 298 | 299 | .button::before { 300 | content: "["; 301 | margin-right: 1ch; 302 | text-decoration: none; 303 | display: inline-block; 304 | } 305 | 306 | .button::after { 307 | content: "]"; 308 | margin-left: 1ch; 309 | text-decoration: none; 310 | display: inline-block; 311 | } 312 | 313 | 314 | .noEvents { 315 | pointer-events: none; 316 | } 317 | 318 | .hasEvents { 319 | pointer-events: all; 320 | } 321 | 322 | 323 | .containBG { 324 | background-size: contain; 325 | background-repeat: no-repeat; 326 | background-position: center; 327 | } 328 | 329 | .coverBG { 330 | background-color: var(--light); 331 | background-size: cover; 332 | background-repeat: no-repeat; 333 | background-position: center; 334 | } 335 | 336 | 337 | .clickHover { 338 | cursor: pointer; 339 | } 340 | 341 | 342 | .whiteFill { 343 | background: var(--white); 344 | } 345 | 346 | .lightFill { 347 | background: var(--light); 348 | color: var(--black); 349 | } 350 | 351 | .grayFill { 352 | background: var(--gray); 353 | } 354 | 355 | .blackFill { 356 | background: var(--black); 357 | } 358 | 359 | .contrastFill { 360 | background: var(--contrast); 361 | color: var(--gray) 362 | } 363 | 364 | 365 | .caption { 366 | font-size: var(--s-1); 367 | } 368 | 369 | 370 | input { 371 | all: unset; 372 | } 373 | 374 | input::placeholder { 375 | color: inherit; 376 | } 377 | 378 | @media screen and (min-width: 1300px) { 379 | 380 | 381 | .halfWidth { 382 | max-inline-size: min(80%, 1200px); 383 | } 384 | 385 | } 386 | 387 | @media screen and (max-width: 600px) { 388 | ::root { 389 | --textWidth: 60vw; 390 | } 391 | 392 | .halfWidth, .quarterWidth { 393 | max-inline-size: 90%; 394 | } 395 | 396 | .padded\:s1 { 397 | padding: var(--s0); 398 | } 399 | 400 | .stack\:s2>*+* { 401 | margin-top: var(--s1); 402 | } 403 | 404 | .center div.dontOverflow { 405 | max-height: 100%; 406 | border: none; 407 | } 408 | .tarotCardContainer { 409 | margin-top: var(--s2); 410 | } 411 | } -------------------------------------------------------------------------------- /styles/tarotCard.css: -------------------------------------------------------------------------------- 1 | .tarotCardContainer { 2 | --aspect: 1.72; 3 | --cardPadding: var(--s-1); 4 | --cardHeight: calc(25 * 1em + 2* var(--cardPadding)); 5 | --cardWidth: calc(29ch + 2* var(--cardPadding)); 6 | } 7 | .tarotCard { 8 | 9 | height: var(--cardHeight); 10 | min-height: var(--cardHeight); 11 | width: var(--cardWidth); 12 | position: relative; 13 | padding: var(--cardPadding); 14 | } 15 | .tarotCardContainer .containHeight { 16 | min-height: var(--s5); 17 | max-height: var(--s5); 18 | } 19 | 20 | .tarotCard p { 21 | width: 90%; 22 | } 23 | 24 | .astral .tarotCard { 25 | --light: #a788b8; 26 | } 27 | 28 | .weave .tarotCard { 29 | 30 | --light: #8bb897; 31 | } 32 | 33 | .flow .tarotCard { 34 | 35 | --light: #dbd7af; 36 | } 37 | 38 | .chime .tarotCard { 39 | --light: #e4b9af; 40 | } 41 | 42 | 43 | 44 | .tarotCard pre code { 45 | color: var(--light); 46 | } 47 | 48 | .tarotCard.lightFill { 49 | background: #dcf9ff; 50 | color: #10163b; 51 | border: 1px solid var(--light); 52 | } 53 | 54 | .tarotCard .contentContainer { 55 | top: 0; 56 | left: 0; 57 | width: 100%; 58 | height: 100%; 59 | position: absolute; 60 | 61 | } 62 | .glow { 63 | box-shadow: 0px var(--s1) var(--s4) rgba(0,0,0,0.2), 0px var(--s-1) var(--s-1) rgba(0,0,0,0.1); 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------