new theme
This commit is contained in:
parent
11754f060e
commit
0ed91dc36d
13 changed files with 130 additions and 51 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
# Skylo Frontend
|
# Skylo Frontend
|
||||||
|
|
||||||
Skylo is a digital card game, which has some similarities to the popular card game Skyjo ;)
|
This is a tribute to the creator of my favorite game, Skyjo! Please purchase the original game to discover the true gaming experience: https://www.magilano.com/produkt/skyjo/
|
||||||
|
|
||||||
Play latest release here: https://skyjo.voltvector.org/
|
Play latest release here: https://skylo-game.com
|
||||||
|
|
||||||
This is the frontend of this web based game using:
|
This is the frontend of this web based game using:
|
||||||
|
|
||||||
- Typescript
|
- Typescript
|
||||||
- React
|
- React
|
||||||
- Vite
|
- Vite
|
||||||
|
|
@ -21,8 +22,6 @@ When two players have joined the same session they can start the game:
|
||||||
Game play:
|
Game play:
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
Backend: https://github.com/pb-coding/skyjo-be
|
Backend: https://github.com/pb-coding/skyjo-be
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
as="style"
|
as="style"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/card-back.svg" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Alegreya:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Play Skylo!</title>
|
<title>Play Skylo!</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -32,7 +32,7 @@
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
"postcss": "^8.4.30",
|
"postcss": "^8.4.31",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.5",
|
"vite": "^4.4.5",
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
"postcss": "^8.4.30",
|
"postcss": "^8.4.31",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.5",
|
"vite": "^4.4.5",
|
||||||
|
|
|
||||||
43
src/components/CardFanAnimation.tsx
Normal file
43
src/components/CardFanAnimation.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { FC, useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
const CardFanAnimation: FC = () => {
|
||||||
|
const cardContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = cardContainerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
const cards = Array.from(container.children) as HTMLImageElement[];
|
||||||
|
const angle = -30;
|
||||||
|
const angleIncrement = 20;
|
||||||
|
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
const calculatedAngle = angle + index * angleIncrement;
|
||||||
|
const rotation = `rotate(${calculatedAngle}deg)`;
|
||||||
|
const translateX = `translate(${index * -40 + 40}px)`;
|
||||||
|
const translateY = `translateY(${
|
||||||
|
Math.abs(calculatedAngle) * 3.5 - 20
|
||||||
|
}px)`;
|
||||||
|
card.style.transform = `${rotation} ${translateX} ${translateY}`;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const imageClasses = "w-40 transition-transform duration-1000";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={cardContainerRef}
|
||||||
|
className="fixed bottom-0 w-full flex justify-center space-x-1"
|
||||||
|
>
|
||||||
|
<img src="/textures/card-7.png" className={imageClasses} alt="Card 7" />
|
||||||
|
<img src="/textures/card-3.png" className={imageClasses} alt="Card 3" />
|
||||||
|
<img src="/textures/card-12.png" className={imageClasses} alt="Card 12" />
|
||||||
|
<img
|
||||||
|
src="/textures/card-minus2.png"
|
||||||
|
className={imageClasses}
|
||||||
|
alt="Card -2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardFanAnimation;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
export const ConnectedIndicator: FC = () => (
|
export const ConnectedIndicator: FC = () => (
|
||||||
<span className="ml-2 flex w-2.5 h-2.5 bg-green-500 rounded-full"></span>
|
<span className="ml-2 flex w-2.5 h-2.5 bg-theme-primary rounded-full border border-black"></span>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DisconnectedIndicator: FC = () => (
|
export const DisconnectedIndicator: FC = () => (
|
||||||
<span className="ml-2 flex w-2 h-2 bg-red-500 rounded-full"></span>
|
<span className="ml-2 flex w-2 h-2 bg-theme-secondary rounded-full"></span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { socket } from "../socket";
|
||||||
|
|
||||||
import { ConnectedIndicator, DisconnectedIndicator } from "./Indicators";
|
import { ConnectedIndicator, DisconnectedIndicator } from "./Indicators";
|
||||||
import Button from "../global/Button";
|
import Button from "../global/Button";
|
||||||
|
import CardFanAnimation from "./CardFanAnimation";
|
||||||
|
|
||||||
type SessionManagerProps = {
|
type SessionManagerProps = {
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
|
@ -48,12 +49,12 @@ export const SessionManager: FC<SessionManagerProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className="w-full h-full absolute top-0 left-0 z-0 bg-teal-500">
|
<div className="w-full h-full absolute top-0 left-0 z-0 bg-gradient-to-b from-theme-bg to-teal-500">
|
||||||
<div className="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 z-10 relative">
|
<div className="font-theme py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 z-10 relative">
|
||||||
<h1 className="mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl lg:text-6xl text-white">
|
<h1 className="drop-shadow-black mb-4 text-7xl font-extrabold tracking-tight leading-none md:text-8xl lg:text-8xl text-white">
|
||||||
Skylo
|
SKYLO
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mb-8 text-lg font-normal lg:text-xl sm:px-16 lg:px-48 text-gray-200">
|
<p className="mb-8 text-lg font-bold lg:text-xl sm:px-16 lg:px-48 text-theme-primary">
|
||||||
Play Skylo online with your friends!
|
Play Skylo online with your friends!
|
||||||
</p>
|
</p>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|
@ -61,13 +62,13 @@ export const SessionManager: FC<SessionManagerProps> = ({
|
||||||
<form onSubmit={joinSession}>
|
<form onSubmit={joinSession}>
|
||||||
<label
|
<label
|
||||||
htmlFor="first_name"
|
htmlFor="first_name"
|
||||||
className="block mb-2 text-sm font-medium text-white"
|
className="block mb-2 text-md font-medium text-theme-font drop-shadow-white"
|
||||||
>
|
>
|
||||||
Join Skylo Session
|
Join Skylo Session
|
||||||
</label>
|
</label>
|
||||||
<div className="flex space-x-1 items-center">
|
<div className="flex space-x-1 items-center">
|
||||||
<input
|
<input
|
||||||
className="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
className="border text-sm rounded-lg block w-full p-2.5 bg-theme-tertiary border-black placeholder-gray-400 text-theme-font focus:ring-theme-primary focus:border-theme-primary"
|
||||||
placeholder="Session name"
|
placeholder="Session name"
|
||||||
required
|
required
|
||||||
onChange={(e) => setSessionField(e.target.value)}
|
onChange={(e) => setSessionField(e.target.value)}
|
||||||
|
|
@ -78,8 +79,13 @@ export const SessionManager: FC<SessionManagerProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-200 text-3xl my-2">Session: {session}</p>
|
<div className="mx-4 py-4 bg-teal-200 border border-black rounded-lg">
|
||||||
<p className="text-gray-200 text-3xl my-4">
|
{isActiveSession && (
|
||||||
|
<p className="text-theme-font text-3xl drop-shadow-white my-2">
|
||||||
|
Session: {session}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-theme-font text-3xl drop-shadow-white mt-4">
|
||||||
Players: {clientsInRoom}
|
Players: {clientsInRoom}
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -91,10 +97,26 @@ export const SessionManager: FC<SessionManagerProps> = ({
|
||||||
Leave Session
|
Leave Session
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div className="mt-8 flex justify-center items-center">
|
</div>
|
||||||
<span className="text-gray-200">Game Server: </span>
|
<div className="mt-4 flex justify-center items-center">
|
||||||
|
<span className="text-theme-font drop-shadow-white">
|
||||||
|
Game Server:{" "}
|
||||||
|
</span>
|
||||||
{isConnected ? <ConnectedIndicator /> : <DisconnectedIndicator />}
|
{isConnected ? <ConnectedIndicator /> : <DisconnectedIndicator />}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="font-bold text-xs text-theme-font drop-shadow-white">
|
||||||
|
This is a tribute to the creator of my favorite game, Skyjo!
|
||||||
|
Please purchase the game to discover the true gaming experience:
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
className="text-theme-primary text-sm font-extrabold"
|
||||||
|
href="https://www.magilano.com/produkt/skyjo/"
|
||||||
|
>
|
||||||
|
Buy Skyjo here
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<CardFanAnimation />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ const VoiceChat: FC<VoiceChatProps> = ({ session }) => {
|
||||||
{/*<audio ref={localAudioRef} autoPlay muted></audio>*/}
|
{/*<audio ref={localAudioRef} autoPlay muted></audio>*/}
|
||||||
<audio ref={remoteAudioRef} autoPlay></audio>
|
<audio ref={remoteAudioRef} autoPlay></audio>
|
||||||
<button onClick={toggleAudio}>
|
<button onClick={toggleAudio}>
|
||||||
<HeadsetIcon />
|
<HeadsetIcon enabled={isAudioEnabled} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const Button: FC<ButtonProps> = ({ variant, children, ...rest }) => {
|
||||||
if (variant === "secondary") {
|
if (variant === "secondary") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="text-white focus:outline-none focus:ring-4 font-medium rounded-lg text-sm px-3 py-2 mr-2 bg-gray-800 hover:bg-gray-700 focus:ring-gray-700 border-gray-700"
|
className="border border-black text-white focus:outline-none focus:ring-4 font-medium rounded-lg text-sm px-3 py-2 mr-2 bg-theme-secondary hover:bg-theme-secondary-hover focus:ring-gray-700"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -18,7 +18,7 @@ const Button: FC<ButtonProps> = ({ variant, children, ...rest }) => {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 bg-lime-700 hover:bg-blue-700 focus:outline-none focus:ring-blue-800"
|
className="border border-black text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 bg-theme-primary hover:bg-theme-primary-hover focus:outline-none focus:ring-blue-800"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ type RoundChipProps = {
|
||||||
|
|
||||||
const RoundChip: FC<RoundChipProps> = ({ description, children }) => {
|
const RoundChip: FC<RoundChipProps> = ({ description, children }) => {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center justify-center w-12 h-12 ml-4 mt-4 text-sm font-semibold rounded-full bg-gray-800 hover:bg-gray-700 text-gray-300">
|
<span className="inline-flex items-center justify-center w-8 h-8 ml-4 mt-4 text-sm font-semibold rounded-full bg-theme-secondary hover:bg-theme-secondary-hover text-white">
|
||||||
{children}
|
{children}
|
||||||
<span className="sr-only">{description}</span>
|
<span className="sr-only">{description}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
const HeadsetIcon: FC = () => {
|
type HeadsetIconProps = {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeadsetIcon: FC<HeadsetIconProps> = ({ enabled }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6 text-white"
|
className="w-4 h-4 pt-0.5 text-white"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
|
@ -11,7 +15,7 @@ const HeadsetIcon: FC = () => {
|
||||||
>
|
>
|
||||||
<path d="M 7.824 5.937 a 1 1 0 0 0 0.726 -0.312 a 2.042 2.042 0 0 1 2.835 -0.065 a 1 1 0 0 0 1.388 -1.441 a 3.994 3.994 0 0 0 -5.674 0.13 a 1 1 0 0 0 0.725 1.688 Z" />
|
<path d="M 7.824 5.937 a 1 1 0 0 0 0.726 -0.312 a 2.042 2.042 0 0 1 2.835 -0.065 a 1 1 0 0 0 1.388 -1.441 a 3.994 3.994 0 0 0 -5.674 0.13 a 1 1 0 0 0 0.725 1.688 Z" />
|
||||||
<path d="M 17 7 A 7 7 0 1 0 3 7 a 3 3 0 0 0 -3 3 v 2 a 3 3 0 0 0 3 3 h 1 a 1 1 0 0 0 1 -1 V 7 a 5 5 0 1 1 10 0 v 7.083 A 2.92 2.92 0 0 1 12.083 17 H 12 a 2 2 0 0 0 -2 -2 H 9 a 2 2 0 0 0 -2 2 v 1 a 2 2 0 0 0 2 2 h 1 a 1.993 1.993 0 0 0 1.722 -1 h 0.361 a 4.92 4.92 0 0 0 4.824 -4 H 17 a 3 3 0 0 0 3 -3 v -2 a 3 3 0 0 0 -3 -3 Z" />
|
<path d="M 17 7 A 7 7 0 1 0 3 7 a 3 3 0 0 0 -3 3 v 2 a 3 3 0 0 0 3 3 h 1 a 1 1 0 0 0 1 -1 V 7 a 5 5 0 1 1 10 0 v 7.083 A 2.92 2.92 0 0 1 12.083 17 H 12 a 2 2 0 0 0 -2 -2 H 9 a 2 2 0 0 0 -2 2 v 1 a 2 2 0 0 0 2 2 h 1 a 1.993 1.993 0 0 0 1.722 -1 h 0.361 a 4.92 4.92 0 0 0 4.824 -4 H 17 a 3 3 0 0 0 3 -3 v -2 a 3 3 0 0 0 -3 -3 Z" />
|
||||||
<path d="M 1 19 L 19 0 L 19 1 L 1 20 L 1 19" />
|
{!enabled && <path d="M 1 19 L 19 0 L 19 1 L 1 20 L 1 19" />}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
import colors from "tailwindcss/colors";
|
|
||||||
|
import color from "tailwindcss/colors";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./node_modules/flowbite-react/**/*.js",
|
"./node_modules/flowbite-react/**/*.js",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
|
@ -18,18 +21,21 @@ export default {
|
||||||
95: "0.95",
|
95: "0.95",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: "transparent",
|
"theme-bg": color.teal[400],
|
||||||
current: "currentColor",
|
"theme-primary": color.green[800],
|
||||||
black: colors.black,
|
"theme-primary-hover": color.green[700],
|
||||||
white: colors.white,
|
"theme-secondary": color.red[400],
|
||||||
emerald: colors.emerald,
|
"theme-secondary-hover": color.red[300],
|
||||||
indigo: colors.indigo,
|
"theme-tertiary": color.teal[200],
|
||||||
yellow: colors.yellow,
|
"theme-font": color.gray[800],
|
||||||
stone: colors.stone,
|
"theme-accent": color.yellow[500],
|
||||||
sky: colors.sky,
|
},
|
||||||
neutral: colors.neutral,
|
fontFamily: {
|
||||||
gray: colors.gray,
|
theme: ["Alegreya", "serif"],
|
||||||
slate: colors.slate,
|
},
|
||||||
|
dropShadow: {
|
||||||
|
white: "2px 2px 2px rgba(255, 255, 255, 1)",
|
||||||
|
black: "2px 2px 2px rgba(0, 0, 0, 1)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue