mirror of
https://github.com/status-im/js-waku-examples.git
synced 2025-02-15 00:18:14 +00:00
feat: add tictactoe example by hackyguru
This commit is contained in:
parent
504bcd4431
commit
35ca19a78f
@ -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
1
ci/Jenkinsfile
vendored
@ -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() } } }
|
||||
}
|
||||
}
|
||||
|
||||
|
3
examples/tictactoe/.eslintrc.json
Normal file
3
examples/tictactoe/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
32
examples/tictactoe/.gitignore
vendored
Normal file
32
examples/tictactoe/.gitignore
vendored
Normal 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
|
3
examples/tictactoe/README.md
Normal file
3
examples/tictactoe/README.md
Normal 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
|
22
examples/tictactoe/components/Header.js
Normal file
22
examples/tictactoe/components/Header.js
Normal 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>
|
||||
)
|
||||
}
|
104
examples/tictactoe/components/Hero.js
Normal file
104
examples/tictactoe/components/Hero.js
Normal 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>
|
||||
);
|
||||
}
|
20
examples/tictactoe/components/Loading.js
Normal file
20
examples/tictactoe/components/Loading.js
Normal 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>
|
||||
)
|
||||
}
|
283
examples/tictactoe/components/Room.js
Normal file
283
examples/tictactoe/components/Room.js
Normal 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>
|
||||
);
|
||||
}
|
7
examples/tictactoe/jsconfig.json
Normal file
7
examples/tictactoe/jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
6
examples/tictactoe/next.config.js
Normal file
6
examples/tictactoe/next.config.js
Normal 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
6066
examples/tictactoe/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
examples/tictactoe/package.json
Normal file
28
examples/tictactoe/package.json
Normal 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"
|
||||
}
|
||||
}
|
18
examples/tictactoe/pages/_app.js
Normal file
18
examples/tictactoe/pages/_app.js
Normal 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>
|
||||
)
|
||||
}
|
13
examples/tictactoe/pages/_document.js
Normal file
13
examples/tictactoe/pages/_document.js
Normal 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>
|
||||
)
|
||||
}
|
5
examples/tictactoe/pages/api/hello.js
Normal file
5
examples/tictactoe/pages/api/hello.js
Normal 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' })
|
||||
}
|
32
examples/tictactoe/pages/game/[id].js
Normal file
32
examples/tictactoe/pages/game/[id].js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
22
examples/tictactoe/pages/index.js
Normal file
22
examples/tictactoe/pages/index.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
6
examples/tictactoe/postcss.config.js
Normal file
6
examples/tictactoe/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
examples/tictactoe/public/favicon.ico
Normal file
BIN
examples/tictactoe/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
3
examples/tictactoe/public/logo.svg
Normal file
3
examples/tictactoe/public/logo.svg
Normal 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 |
1
examples/tictactoe/public/next.svg
Normal file
1
examples/tictactoe/public/next.svg
Normal 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 |
1
examples/tictactoe/public/thirteen.svg
Normal file
1
examples/tictactoe/public/thirteen.svg
Normal 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 |
1
examples/tictactoe/public/vercel.svg
Normal file
1
examples/tictactoe/public/vercel.svg
Normal 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 |
278
examples/tictactoe/styles/Home.module.css
Normal file
278
examples/tictactoe/styles/Home.module.css
Normal 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);
|
||||
}
|
||||
}
|
3
examples/tictactoe/styles/globals.css
Normal file
3
examples/tictactoe/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
15
examples/tictactoe/tailwind.config.js
Normal file
15
examples/tictactoe/tailwind.config.js
Normal 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: [],
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user