feat: add tictactoe example by hackyguru

This commit is contained in:
Václav Pavlín 2023-12-08 11:28:54 +01:00
parent 504bcd4431
commit 35ca19a78f
No known key found for this signature in database
GPG Key ID: B378FB31BB6D89A5
27 changed files with 6979 additions and 0 deletions

View File

@ -12,6 +12,12 @@ See https://examples.waku.org/ for more examples.
- [website](https://examples.waku.org/web-chat)
- Demonstrates: Group chat, React/TypeScript, Relay, Store.
### Tic Tac Toe
- [code](examples/tictactoe)
- [website](https://examples.waku.org/tictactoe)
- Demonstrates: Gaming, Light Client, Store, NextJS, Waku React.
### Ethereum Private Messaging
End-to-end encrypted communication between two Ethereum addresses.

1
ci/Jenkinsfile vendored
View File

@ -44,6 +44,7 @@ pipeline {
stage('noise-rtc') { steps { script { buildExample() } } }
stage('relay-direct-rtc') { steps { script { buildExample() } } }
stage('rln-js') { steps { script { buildNextJSExample() } } }
stage('tictactoe') { steps { script { buildNextJSExample() } } }
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

32
examples/tictactoe/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel

View File

@ -0,0 +1,3 @@
# TicTacToe with Waku
This repository is a basic implementation of a TicTacToe game where 2 players can join a particular game id which operates through a content topic on top of Waku

View File

@ -0,0 +1,22 @@
import Link from 'next/link'
import React from 'react'
export default function Header() {
return (
<div className='flex p-5 justify-between items-center'>
<Link href="/">
<div id='logo'>
<svg width="60" height="60" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M169.344 35.844A9.5 9.5 0 0 0 160 45.47v117.343H45.5a9.5 9.5 0 1 0 0 19H160v151H45.5a9.5 9.5 0 1 0 0 19H160V466.5a9.5 9.5 0 1 0 19 0V351.812h151V466.5a9.5 9.5 0 1 0 19 0V351.812h117.406a9.5 9.5 0 1 0 0-19H349v-151h115.563a9.5 9.5 0 1 0 0-19H349V45.47a9.5 9.5 0 0 0-9.656-9.626A9.5 9.5 0 0 0 330 45.47v117.343H179V45.47a9.5 9.5 0 0 0-9.656-9.626zM86 35.97c-13.07 0-25.77 4.94-35.156 13.843C41.458 58.715 36 71.06 36 83.874s5.458 25.16 14.844 34.063C60.23 126.84 72.93 131.81 86 131.81s25.77-4.97 35.156-13.875C130.542 109.034 136 96.69 136 83.876c0-12.814-5.458-25.16-14.844-34.063C111.77 40.91 99.07 35.97 86 35.97zm170 0c-13.07 0-25.77 4.94-35.156 13.843C211.458 58.715 206 71.06 206 83.874s5.458 25.16 14.844 34.063C230.23 126.84 242.93 131.81 256 131.81s25.77-4.97 35.156-13.875C300.542 109.034 306 96.69 306 83.876c0-12.814-5.458-25.16-14.844-34.063C281.77 40.91 269.07 35.97 256 35.97zm138.844 9.218A9.5 9.5 0 0 0 388.25 61.5l22.375 22.375L389 105.5a9.502 9.502 0 1 0 13.438 13.438l21.625-21.626l22.375 22.407a9.502 9.502 0 1 0 13.437-13.44L437.5 83.876l21.625-21.625a9.5 9.5 0 0 0-6.906-16.313a9.5 9.5 0 0 0-6.533 2.876l-21.625 21.624l-22.375-22.374a9.5 9.5 0 0 0-6.843-2.876zM86 54.968c8.137 0 16.485 3.337 22.094 8.657c5.608 5.32 8.937 12.95 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.304-22.094-8.624c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.302 3.328-14.93 8.936-20.25c5.61-5.32 13.957-8.657 22.094-8.657zm170 0c8.137 0 16.485 3.337 22.094 8.657c5.608 5.32 8.937 12.95 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.304-22.094-8.624c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.302 3.328-14.93 8.936-20.25c5.61-5.32 13.957-8.657 22.094-8.657zm-77 126.844h151v151H179v-151zm245.063 26.282c-13.07 0-25.77 4.94-35.157 13.844c-9.386 8.903-14.844 21.248-14.844 34.062c0 12.814 5.458 25.16 14.844 34.063c9.386 8.903 22.087 13.875 35.156 13.875c13.07 0 25.77-4.972 35.157-13.875c9.385-8.904 14.842-21.25 14.842-34.063c0-12.814-5.457-25.16-14.843-34.063c-9.387-8.903-22.088-13.843-35.158-13.843zm-197.25 9.22a9.5 9.5 0 0 0-6.625 16.31L242.563 256l-21.625 21.625a9.502 9.502 0 1 0 13.437 13.438L256 269.438l22.375 22.375a9.502 9.502 0 1 0 13.438-13.438L269.438 256l21.625-21.625a9.5 9.5 0 0 0-6.907-16.313a9.5 9.5 0 0 0-6.53 2.875L256 242.563l-22.375-22.375a9.5 9.5 0 0 0-6.813-2.875zm197.25 9.78c8.136 0 16.485 3.305 22.093 8.625c5.61 5.32 8.938 12.98 8.938 20.28c0 7.3-3.33 14.93-8.938 20.25c-5.608 5.32-13.957 8.656-22.094 8.656c-8.136 0-16.485-3.336-22.093-8.656c-5.61-5.32-8.94-12.95-8.94-20.25c0-7.3 3.33-14.96 8.94-20.28c5.607-5.32 13.956-8.626 22.092-8.626zM256 380.156c-13.07 0-25.77 4.94-35.156 13.844c-9.386 8.903-14.844 21.25-14.844 34.063c0 12.813 5.458 25.19 14.844 34.093C230.23 471.06 242.93 476.03 256 476.03s25.77-4.97 35.156-13.874c9.386-8.903 14.844-21.28 14.844-34.094c0-12.813-5.458-25.16-14.844-34.062c-9.386-8.903-22.087-13.844-35.156-13.844zm-199.188 9.22a9.5 9.5 0 0 0-6.624 16.312l22.374 22.406L50.94 449.72a9.502 9.502 0 1 0 13.437 13.436L86 441.53l22.375 22.376a9.502 9.502 0 1 0 13.438-13.437l-22.376-22.376l21.626-21.625a9.5 9.5 0 0 0-6.907-16.314a9.5 9.5 0 0 0-6.53 2.875L86 414.657L63.625 392.25a9.5 9.5 0 0 0-6.813-2.875zM256 399.187c8.137 0 16.485 3.304 22.094 8.625c5.608 5.32 8.937 12.948 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.306-22.094-8.626c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.303 3.328-14.93 8.936-20.252c5.61-5.32 13.957-8.625 22.094-8.625z" />
</svg>
</div>
</Link>
<a href='https://github.com/hackyguru/tictactoe' className='text-white flex space-x-3 items-center cursor-pointer'>
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2Z" />
</svg>
<p>GitHub</p>
</a>
</div>
)
}

View File

@ -0,0 +1,104 @@
import React, { useState, useEffect } from 'react';
import Loading from './Loading';
import ShortUniqueId from 'short-unique-id';
import Router from 'next/router';
export default function Hero() {
const options = { length: 8 };
const uid = new ShortUniqueId(options);
const [room, setRoom] = useState(null);
const [game, setGame] = useState(null);
const [joinLink, setJoinLink] = useState('');
useEffect(() => {
if (room === null) {
setRoom(uid.rnd());
}
}, []);
if (room === null) {
return <Loading />;
}
const handleNewGameClick = () => {
sessionStorage.setItem('roomId', room);
sessionStorage.setItem('player', 'x');
Router.push(`/game/${room}`);
};
const handleJoinGameClick = () => {
setGame('join');
};
const handleJoinLinkChange = (e) => {
setJoinLink(e.target.value);
};
const handleJoinButtonClick = () => {
Router.push(`/game/${joinLink}`);
};
const handleGoBackClick = () => {
setGame(null);
};
return (
<section id='hero'>
<div className="mx-auto max-w-screen-xl px-4 lg:flex lg:mt-40 lg:items-center mt-40">
<div className="mx-auto max-w-xl text-center">
<h1 className="text-3xl sm:text-8xl text-white">TicTacToe</h1>
<div className='mt-8 sm:text-xl/relaxed text-white opacity-60 flex items-center justify-center'>
<p>Built with</p>
<img src='https://waku.org/theme/image/logo.svg' />
<a href='https://waku.org' className='underline'>
Waku
</a>
</div>
{game === null && (
<div className="mt-20 flex flex-wrap justify-center gap-4">
<button
className="block w-full bg-white px-12 py-3 text-sm font-medium hover:text-white hover:bg-black hover:border-2 hover:border-white focus:outline-none focus:ring text-black"
onClick={handleNewGameClick}
>
New game
</button>
<button
onClick={handleJoinGameClick}
className="block w-full bg-white px-12 py-3 text-sm font-medium hover:text-white hover:bg-black hover:border-2 hover:border-white focus:outline-none focus:ring text-black"
>
Join game
</button>
</div>
)}
{game === 'join' && (
<div className="mt-20 space-y-4">
<input
value={joinLink}
onChange={handleJoinLinkChange}
className="px-3 py-3 border-2 border-white w-full"
placeholder='Enter the game link'
/>
<div className='flex space-x-5 items-center'>
<button
onClick={handleJoinButtonClick}
className="block w-full bg-white px-12 py-3 text-sm font-medium hover:text-white hover:bg-black hover:border-2 hover:border-white focus:outline-none focus:ring text-black"
>
Join game
</button>
<button
onClick={handleGoBackClick}
className="block w-full bg-white px-12 py-3 text-sm font-medium hover:text-white hover:bg-black hover:border-2 hover:border-white focus:outline-none focus:ring text-black"
>
Go back
</button>
</div>
</div>
)}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,20 @@
import React from 'react'
export default function Loading() {
return (
<div className='h-screen bg-black flex items-center w-full justify-center'>
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#ffffff" strokeLinecap="round" strokeWidth="2">
<path strokeDasharray="60" strokeDashoffset="60" strokeOpacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z">
<animate fill="freeze" attributeName="strokeDashoffset" dur="1.3s" values="60;0" />
</path>
<path strokeDasharray="15" strokeDashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12">
<animate fill="freeze" attributeName="strokeDashoffset" dur="0.3s" values="15;0" />
<animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12" />
</path>
</g>
</svg>
</div>
)
}

View File

@ -0,0 +1,283 @@
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import copy from 'copy-to-clipboard';
import protobuf from 'protobufjs';
import {
useWaku,
useContentPair,
useLightPush,
useStoreMessages,
useFilterMessages,
} from '@waku/react';
import { message } from '@waku/core';
import Loading from './Loading';
const ChatMessage = new protobuf.Type('ChatMessage')
.add(new protobuf.Field('timestamp', 1, 'uint64'))
.add(new protobuf.Field('sender', 2, 'string'))
.add(new protobuf.Field('message', 3, 'string'));
export default function Room(props) {
const { node } = useWaku();
const [nodeStart, setNodeStart] = useState(false);
const [move, setMove] = useState(false);
const [boxes, setBoxes] = useState({});
const [player, setPlayer] = useState(false);
const [opponentJoined, setOpponentJoined] = useState(null);
const [winner, setWinner] = useState(null);
const [winningPattern, setWinningPattern] = useState(null);
const { decoder, encoder } = useContentPair();
const { messages: storeMessages } = useStoreMessages({
node,
decoder,
});
const { messages: filterMessages } = useFilterMessages({ node, decoder });
const { push } = useLightPush({ node, encoder });
async function sendMessage(sender, message) {
const protoMessage = ChatMessage.create({
timestamp: Date.now(),
sender,
message,
});
const serialisedMessage = ChatMessage.encode(protoMessage).finish();
const timestamp = new Date();
await push({
payload: serialisedMessage,
timestamp,
});
console.log('MESSAGE PUSHED');
}
function decodeMessage(wakuMessage) {
if (!wakuMessage.payload) return;
const { timestamp, sender, message } = ChatMessage.decode(wakuMessage.payload);
if (!timestamp || !sender || !message) return;
const time = new Date();
time.setTime(Number(timestamp));
return {
message,
timestamp: time,
sender,
timestampInt: wakuMessage.timestamp,
};
}
useEffect(() => {
if (node !== undefined) {
if (player === false) {
const p =
sessionStorage.getItem('roomId') == props.room && sessionStorage.getItem('player') == 'x'
? 'x'
: 'o';
setPlayer(p);
if (p === 'o') {
sendMessage('o', 'joined');
}
}
setNodeStart(true);
}
}, [node]);
useEffect(() => {
let messages = storeMessages.concat(filterMessages);
let b = {};
let o = false;
messages = messages.map((message) => decodeMessage(message));
messages.forEach((message) => {
if (message.message === 'joined') {
o = true;
return;
}
if (message.message === 'winner') {
return;
}
b = { ...b, [message.message]: message.sender };
});
const winningCombinations = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['1', '4', '7'],
['2', '5', '8'],
['3', '6', '9'],
['1', '5', '9'],
['3', '5', '7'],
];
let winner = null;
let temp = null;
let winningPattern = null;
winningCombinations.forEach((combination) => {
if (winner !== null) {
return;
}
for (let [i, c] of combination.entries()) {
if (b[c] === undefined) {
temp = null;
break;
} else {
if (temp === null) {
temp = b[c];
continue;
} else {
if (temp === b[c]) {
if (i === 2) {
winner = temp;
winningPattern = combination;
}
continue;
}
}
}
}
});
setWinner(winner);
setWinningPattern(winningPattern);
setOpponentJoined(o);
setBoxes(b);
}, [storeMessages, filterMessages]);
if (!nodeStart || !player || opponentJoined === null) {
return <Loading />;
}
function handlePlay(i) {
if (opponentJoined === false) {
alert('Opponent is yet to join!');
return;
}
if (boxes[i] !== undefined) {
alert('Already played!');
return;
}
if (Object.keys(boxes).length % 2 === 0) {
if (player === 'o') {
alert("Opponent's turn");
} else {
sendMessage('x', i);
setBoxes({ ...boxes, i: 'x' });
}
} else {
if (player === 'x') {
alert("Opponent's turn");
} else {
sendMessage('o', i);
setBoxes({ ...boxes, i: 'o' });
}
}
}
function renderBoxes() {
let boxElements = [];
for (let i = 1; i < 10; i += 1) {
if (boxes[i] === undefined) {
boxElements.push(
<div
onClick={() => handlePlay(i.toString())}
className='w-20 h-20 border-2 border-white flex items-center justify-center text-2xl text-white'
></div>
);
} else {
if (boxes[i] === 'x') {
boxElements.push(
<div className='w-20 h-20 border-2 border-white flex items-center justify-center text-2xl text-white'>
X
</div>
);
} else {
boxElements.push(
<div className='w-20 h-20 border-2 border-white flex items-center justify-center text-2xl text-white'>
O
</div>
);
}
}
}
return <div id='tiles' className='grid grid-cols-3 grid-rows-3 gap-3 mt-10'>{boxElements}</div>;
}
return (
<section id='room'>
<div className='flex justify-end mr-5 text-white space-x-5 items-center'>
<p>Waku status : active</p>
<svg width='20' height='20' viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg'>
<g fill='none' stroke='currentColor' strokeWidth='4'>
<path d='M13.5 39.37A16.927 16.927 0 0 0 24 43c3.963 0 7.61-1.356 10.5-3.63M19 9.747C12.051 11.882 7 18.351 7 26c0 1.925.32 3.775.91 5.5M29 9.747C35.949 11.882 41 18.351 41 26c0 1.925-.32 3.775-.91 5.5' />
<path strokeLinecap='round' strokeLinejoin='round' d='M43 36c0 1.342-.528 2.56-1.388 3.458A5 5 0 1 1 43 36Zm-28 0c0 1.342-.528 2.56-1.388 3.458A5 5 0 1 1 15 36ZM29 9c0 1.342-.528 2.56-1.388 3.458A5 5 0 1 1 29 9Z' />
</g>
</svg>
<p>Peers : {node?.libp2p?.getPeers()?.length ?? '-'}</p>
</div>
<div className='mx-auto max-w-screen-xl px-4 mt-20 lg:flex lg:h-mt-40 lg:items-center'>
<div className='mx-auto max-w-xl'>
<div className='flex space-x-2 text-white opacity-60 mb-5 items-center'>
<Link href='/'>
<svg width='20' height='20' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'>
<path fill='currentColor' d='M224 480h640a32 32 0 1 1 0 64H224a32 32 0 0 1 0-64z' />
<path
fill='currentColor'
d='m237.248 512l265.408 265.344a32 32 0 0 1-45.312 45.312l-288-288a32 32 0 0 1 0-45.312l288-288a32 32 0 1 1 45.312 45.312L237.248 512z'
/>
</svg>
</Link>
<p>Game URL</p>
</div>
<div className='px-3 py-3 text-center flex border-2 border-white text-white underline items-center justify-between'>
<p>https://waku-xo.vercel.app/game/{props.room}</p>
<button onClick={() => copy(`https://waku-xo.vercel.app/game/${props.room}`)}>
<svg width='20' height='20' viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'>
<path
fill='currentColor'
d='M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z'
/>
</svg>
</button>
</div>
<h1 className='text-3xl sm:text-4xl text-white mt-20'>
{!opponentJoined && 'Waiting for opponent to join'}
{winner == null &&
opponentJoined &&
(Object.keys(boxes).length % 2 === 0
? player === 'x'
? 'Your turn'
: "Opponent's turn"
: player === 'o'
? 'Your turn'
: "Opponent's turn")}
{winner != null && (winner === 'x' ? 'X is winner' : 'O is winner')}
</h1>
{
!winner &&
<div className='flex justify-center'>{renderBoxes()}</div>
}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

6066
examples/tictactoe/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "tictactoe",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@waku/react": "^0.0.5-77c40b4",
"@waku/sdk": "^0.0.19",
"copy-to-clipboard": "^3.3.3",
"eslint": "8.52.0",
"eslint-config-next": "13.5.6",
"next": "13.5.6",
"protobufjs": "^7.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"short-unique-id": "^5.0.3"
},
"devDependencies": {
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3"
}
}

View File

@ -0,0 +1,18 @@
import '@/styles/globals.css'
// Waku imports
import { LightNodeProvider } from "@waku/react";
import { Protocols } from "@waku/sdk";
export default function App({ Component, pageProps }) {
return (
<LightNodeProvider
options={{ defaultBootstrap: true }}
protocols={[Protocols.Store, Protocols.Filter, Protocols.LightPush]}
>
<Component {...pageProps} />
</LightNodeProvider>
)
}

View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View File

@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}

View File

@ -0,0 +1,32 @@
import Header from '@/components/Header'
import Room from '@/components/Room'
import React, { useState, useEffect } from 'react'
import Loading from '@/components/Loading';
import { useRouter } from 'next/router'
// Waku imports
import { ContentPairProvider, useWaku } from "@waku/react";
// Misc imports
import ShortUniqueId from 'short-unique-id';
export default function Game() {
const router = useRouter();
const room = router.query.id;
return (
<>
<ContentPairProvider
contentTopic={"/tictactoe/" + room}
>
<div className='bg-black h-screen'>
<Header />
<Room room={room} />
</div>
</ContentPairProvider>
</>
)
}

View File

@ -0,0 +1,22 @@
import Head from 'next/head'
import Hero from '@/components/Hero'
import Header from '@/components/Header'
export default function Home() {
return (
<>
<Head>
<title>Tic Tac Toe</title>
<meta name="description" content="Tic Tac Toe game created with Waku" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className='bg-black h-screen'>
<Header />
<Hero />
</main>
</>
)
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M169.344 35.844A9.5 9.5 0 0 0 160 45.47v117.343H45.5a9.5 9.5 0 1 0 0 19H160v151H45.5a9.5 9.5 0 1 0 0 19H160V466.5a9.5 9.5 0 1 0 19 0V351.812h151V466.5a9.5 9.5 0 1 0 19 0V351.812h117.406a9.5 9.5 0 1 0 0-19H349v-151h115.563a9.5 9.5 0 1 0 0-19H349V45.47a9.5 9.5 0 0 0-9.656-9.626A9.5 9.5 0 0 0 330 45.47v117.343H179V45.47a9.5 9.5 0 0 0-9.656-9.626zM86 35.97c-13.07 0-25.77 4.94-35.156 13.843C41.458 58.715 36 71.06 36 83.874s5.458 25.16 14.844 34.063C60.23 126.84 72.93 131.81 86 131.81s25.77-4.97 35.156-13.875C130.542 109.034 136 96.69 136 83.876c0-12.814-5.458-25.16-14.844-34.063C111.77 40.91 99.07 35.97 86 35.97zm170 0c-13.07 0-25.77 4.94-35.156 13.843C211.458 58.715 206 71.06 206 83.874s5.458 25.16 14.844 34.063C230.23 126.84 242.93 131.81 256 131.81s25.77-4.97 35.156-13.875C300.542 109.034 306 96.69 306 83.876c0-12.814-5.458-25.16-14.844-34.063C281.77 40.91 269.07 35.97 256 35.97zm138.844 9.218A9.5 9.5 0 0 0 388.25 61.5l22.375 22.375L389 105.5a9.502 9.502 0 1 0 13.438 13.438l21.625-21.626l22.375 22.407a9.502 9.502 0 1 0 13.437-13.44L437.5 83.876l21.625-21.625a9.5 9.5 0 0 0-6.906-16.313a9.5 9.5 0 0 0-6.533 2.876l-21.625 21.624l-22.375-22.374a9.5 9.5 0 0 0-6.843-2.876zM86 54.968c8.137 0 16.485 3.337 22.094 8.657c5.608 5.32 8.937 12.95 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.304-22.094-8.624c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.302 3.328-14.93 8.936-20.25c5.61-5.32 13.957-8.657 22.094-8.657zm170 0c8.137 0 16.485 3.337 22.094 8.657c5.608 5.32 8.937 12.95 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.304-22.094-8.624c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.302 3.328-14.93 8.936-20.25c5.61-5.32 13.957-8.657 22.094-8.657zm-77 126.844h151v151H179v-151zm245.063 26.282c-13.07 0-25.77 4.94-35.157 13.844c-9.386 8.903-14.844 21.248-14.844 34.062c0 12.814 5.458 25.16 14.844 34.063c9.386 8.903 22.087 13.875 35.156 13.875c13.07 0 25.77-4.972 35.157-13.875c9.385-8.904 14.842-21.25 14.842-34.063c0-12.814-5.457-25.16-14.843-34.063c-9.387-8.903-22.088-13.843-35.158-13.843zm-197.25 9.22a9.5 9.5 0 0 0-6.625 16.31L242.563 256l-21.625 21.625a9.502 9.502 0 1 0 13.437 13.438L256 269.438l22.375 22.375a9.502 9.502 0 1 0 13.438-13.438L269.438 256l21.625-21.625a9.5 9.5 0 0 0-6.907-16.313a9.5 9.5 0 0 0-6.53 2.875L256 242.563l-22.375-22.375a9.5 9.5 0 0 0-6.813-2.875zm197.25 9.78c8.136 0 16.485 3.305 22.093 8.625c5.61 5.32 8.938 12.98 8.938 20.28c0 7.3-3.33 14.93-8.938 20.25c-5.608 5.32-13.957 8.656-22.094 8.656c-8.136 0-16.485-3.336-22.093-8.656c-5.61-5.32-8.94-12.95-8.94-20.25c0-7.3 3.33-14.96 8.94-20.28c5.607-5.32 13.956-8.626 22.092-8.626zM256 380.156c-13.07 0-25.77 4.94-35.156 13.844c-9.386 8.903-14.844 21.25-14.844 34.063c0 12.813 5.458 25.19 14.844 34.093C230.23 471.06 242.93 476.03 256 476.03s25.77-4.97 35.156-13.874c9.386-8.903 14.844-21.28 14.844-34.094c0-12.813-5.458-25.16-14.844-34.062c-9.386-8.903-22.087-13.844-35.156-13.844zm-199.188 9.22a9.5 9.5 0 0 0-6.624 16.312l22.374 22.406L50.94 449.72a9.502 9.502 0 1 0 13.437 13.436L86 441.53l22.375 22.376a9.502 9.502 0 1 0 13.438-13.437l-22.376-22.376l21.626-21.625a9.5 9.5 0 0 0-6.907-16.314a9.5 9.5 0 0 0-6.53 2.875L86 414.657L63.625 392.25a9.5 9.5 0 0 0-6.813-2.875zM256 399.187c8.137 0 16.485 3.304 22.094 8.625c5.608 5.32 8.937 12.948 8.937 20.25c0 7.3-3.328 14.96-8.936 20.28c-5.61 5.32-13.957 8.626-22.094 8.626s-16.485-3.306-22.094-8.626c-5.608-5.32-8.937-12.98-8.937-20.28c0-7.303 3.328-14.93 8.936-20.252c5.61-5.32 13.957-8.625 22.094-8.625z" />
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,278 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo,
.thirteen {
position: relative;
}
.thirteen {
display: flex;
justify-content: center;
align-items: center;
width: 75px;
height: 75px;
padding: 25px 10px;
margin-left: 16px;
transform: translateZ(0);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0px 2px 8px -1px #0000001a;
}
.thirteen::before,
.thirteen::after {
content: '';
position: absolute;
z-index: -1;
}
/* Conic Gradient Animation */
.thirteen::before {
animation: 6s rotate linear infinite;
width: 200%;
height: 200%;
background: var(--tile-border);
}
/* Inner Square */
.thirteen::after {
inset: 0;
padding: 1px;
border-radius: var(--border-radius);
background: linear-gradient(
to bottom right,
rgba(var(--tile-start-rgb), 1),
rgba(var(--tile-end-rgb), 1)
);
background-clip: content-box;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.thirteen::before {
animation: none;
}
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo,
.thirteen img {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
// Or if using `src` directory:
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}