diff --git a/Components/AccountDialog.jsx b/Components/AccountDialog.jsx new file mode 100644 index 00000000..afff70ff --- /dev/null +++ b/Components/AccountDialog.jsx @@ -0,0 +1,68 @@ +import React, { useState, useContext, useEffect, useRef } from "react"; + +import { ChatAppContext } from "../Context/ChatAppContext"; +import Spinner from "./Spinner"; + +const AccountDialog = ({ onClose }) => { + const [name, setName] = useState(""); + const { createAccount, busy } = useContext(ChatAppContext); + const working = busy === "account"; + const input = useRef(null); + + useEffect(() => { + input.current?.focus(); + const onKey = (e) => e.key === "Escape" && !working && onClose(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose, working]); + + const submit = async (e) => { + e.preventDefault(); + if (await createAccount({ name })) onClose(); + }; + + return ( +
+
+

Create your account

+

+ Pick the name your friends will see. It is stored on the chain, so + MetaMask will ask you to confirm one transaction. +

+
+ + setName(e.target.value)} + /> +
+
+ + +
+
+
+ ); +}; + +export default AccountDialog; diff --git a/Components/Avatar.jsx b/Components/Avatar.jsx new file mode 100644 index 00000000..52137b65 --- /dev/null +++ b/Components/Avatar.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +// A colour and an initial derived from the address, so the same person looks +// the same everywhere without storing an image. +const hue = (seed) => { + let hash = 0; + for (let i = 0; i < seed.length; i += 1) { + hash = (hash * 31 + seed.charCodeAt(i)) % 360; + } + return hash; +}; + +const Avatar = ({ name = "", address = "", size = 40 }) => ( + +); + +export default Avatar; diff --git a/Components/EmptyState.jsx b/Components/EmptyState.jsx new file mode 100644 index 00000000..52088e2b --- /dev/null +++ b/Components/EmptyState.jsx @@ -0,0 +1,11 @@ +import React from "react"; + +const EmptyState = ({ title, text, children }) => ( +
+

{title}

+

{text}

+ {children} +
+); + +export default EmptyState; diff --git a/Components/Error/Error.jsx b/Components/Error/Error.jsx deleted file mode 100644 index 41b6c167..00000000 --- a/Components/Error/Error.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import styles from "./Error.module.css"; - -const Error = ({ error, onClose }) => { - return ( -
-
- -

Something Went Wrong

-

- Please fix the following issue and try again: -

-

{error}

- -
-
- ); -}; - -export default Error; diff --git a/Components/Error/Error.module.css b/Components/Error/Error.module.css deleted file mode 100644 index 9975c12f..00000000 --- a/Components/Error/Error.module.css +++ /dev/null @@ -1,129 +0,0 @@ -.overlay { - position: fixed; - inset: 0; - z-index: 10000; - background: rgba(15, 18, 31, 0.6); - backdrop-filter: blur(12px); - display: flex; - justify-content: center; - align-items: center; - animation: fadeIn 0.5s ease-in-out; -} - -.card { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 20px; - padding: 2.5rem; - width: 90%; - max-width: 420px; - text-align: center; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); - animation: slideUp 0.6s ease-in-out; -} - -.icon { - width: 3.5rem; - height: 3.5rem; - margin: 0 auto; - border: 3px solid #f59e0b; - border-radius: 50%; - color: #f59e0b; - font-size: 2rem; - font-weight: 800; - line-height: 3rem; - animation: bounce 1.5s infinite; -} - -.title { - font-size: 1.8rem; - font-weight: 700; - color: #fff; - margin-top: 1rem; - letter-spacing: 0.5px; -} - -.message { - color: #ccc; - font-size: 1rem; - margin-top: 0.5rem; -} - -.errorText { - margin-top: 1rem; - font-size: 1.05rem; - color: #ffcc70; - background: rgba(255, 204, 112, 0.1); - border: 1px solid rgba(255, 204, 112, 0.4); - border-radius: 8px; - padding: 0.75rem 1rem; - word-break: break-word; -} - -.reloadButton { - margin-top: 1.5rem; - background: linear-gradient(90deg, #ff8a00, #ff5f6d); - border: none; - color: white; - font-weight: 600; - font-size: 1rem; - padding: 0.75rem 2rem; - border-radius: 30px; - cursor: pointer; - transition: all 0.3s ease-in-out; - box-shadow: 0 4px 10px rgba(255, 94, 109, 0.3); -} - -.reloadButton:hover { - transform: translateY(-3px); - box-shadow: 0 6px 14px rgba(255, 94, 109, 0.5); -} - -@keyframes slideUp { - from { - transform: translateY(60px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -@keyframes fadeIn { - from { - background: rgba(15, 18, 31, 0); - } - to { - background: rgba(15, 18, 31, 0.6); - } -} - -@keyframes bounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-8px); - } -} - -@media only screen and (max-width: 35em) { - .card { - padding: 1.5rem; - border-radius: 16px; - } - - .title { - font-size: 1.4rem; - } - - .errorText { - font-size: 0.9rem; - } - - .reloadButton { - font-size: 0.9rem; - padding: 0.6rem 1.5rem; - } -} diff --git a/Components/Filter/Filter.jsx b/Components/Filter/Filter.jsx deleted file mode 100644 index 968eeb17..00000000 --- a/Components/Filter/Filter.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import styles from "./Filter.module.css"; - -const Filter = () => { - return ( -
-
-

- Welcome to Node Connect, where you can earn tokens by engaging in - chats! You can always check your token balance in the top right - corner. -

-
-
- ); -}; - -export default Filter; diff --git a/Components/Filter/Filter.module.css b/Components/Filter/Filter.module.css deleted file mode 100644 index 4ddfdd87..00000000 --- a/Components/Filter/Filter.module.css +++ /dev/null @@ -1,50 +0,0 @@ -.wrapper { - width: 100%; - display: flex; - justify-content: center; -} - -.filter { - width: 85%; - margin: 2rem 0; - padding: 1.5rem 2rem; - background: rgba(30, 33, 47, 0.6); - backdrop-filter: blur(14px); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); - display: flex; - align-items: center; - justify-content: center; - animation: fadeIn 0.6s ease-in-out; -} - -.infoText { - color: #b0b6c1; - font-size: 1rem; - text-align: center; - line-height: 1.5; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@media only screen and (max-width: 35em) { - .filter { - flex-direction: column; - width: 90%; - padding: 1.2rem; - } - - .infoText { - font-size: 0.95rem; - } -} diff --git a/Components/Friend/Card/Card.jsx b/Components/Friend/Card/Card.jsx deleted file mode 100644 index 116bf32e..00000000 --- a/Components/Friend/Card/Card.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import Link from "next/link"; - -// INTERNAL IMPORT -import styles from "./Card.module.css"; -import images from "../../../assets"; - -const Card = ({ readMessage, el, i, readUser }) => { - return ( -
{ - readMessage(el.pubkey); - readUser(el.pubkey); - }} - > - -
-
-
- user avatar -
-
- -
-
-

{el.name}

-

{el.pubkey.slice(21)}..

-
- -
- {i + 1} -
-
-
- -
- ); -}; - -export default Card; diff --git a/Components/Friend/Card/Card.module.css b/Components/Friend/Card/Card.module.css deleted file mode 100644 index 919db2be..00000000 --- a/Components/Friend/Card/Card.module.css +++ /dev/null @@ -1,144 +0,0 @@ -.card { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1.4rem; - width: 100%; - padding: 1.2rem 1.6rem; - border-radius: 16px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.08); - backdrop-filter: blur(12px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); - cursor: pointer; - transition: all 0.3s ease-in-out; - animation: fadeIn 0.5s ease-in-out; -} - -.card:hover { - transform: translateY(-4px); - box-shadow: 0 8px 28px rgba(0, 217, 255, 0.3); - border-color: rgba(0, 217, 255, 0.25); - background: rgba(255, 255, 255, 0.09); -} - -/* ---- Avatar ---- */ -.avatarSection { - display: flex; - align-items: center; - justify-content: center; -} - -.avatar { - width: 60px; - height: 60px; - border-radius: 50%; - overflow: hidden; - position: relative; - background: radial-gradient(circle, rgba(0, 217, 255, 0.1), rgba(0, 0, 0, 0.2)); - display: flex; - align-items: center; - justify-content: center; - border: 2px solid rgba(0, 217, 255, 0.2); - transition: all 0.3s ease-in-out; -} - -.avatarImage { - object-fit: cover; - border-radius: 50%; - transition: transform 0.3s ease-in-out; -} - -.card:hover .avatarImage { - transform: scale(1.08); -} - -/* ---- Info Section ---- */ -.infoSection { - flex: 1; - display: flex; - justify-content: space-between; - align-items: center; -} - -.infoText { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.infoText h3 { - color: #ffffff; - font-size: 1.15rem; - font-weight: 600; - margin: 0; - letter-spacing: 0.4px; - transition: color 0.3s ease-in-out; -} - -.infoText p { - color: #a5a9b5; - font-size: 0.9rem; - margin: 0; -} - -.card:hover .infoText h3 { - color: #00e0ff; -} - -/* ---- Index Badge ---- */ -.indexBadge span { - background: linear-gradient(135deg, #00e0ff, #0078ff); - color: white; - padding: 0.4rem 0.8rem; - border-radius: 10px; - font-size: 0.9rem; - font-weight: 600; - box-shadow: 0 0 10px rgba(0, 224, 255, 0.3); - transition: all 0.3s ease-in-out; -} - -.card:hover .indexBadge span { - transform: scale(1.1); - box-shadow: 0 0 14px rgba(0, 224, 255, 0.5); -} - -/* ---- Animation ---- */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(25px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ---- Responsive ---- */ -@media only screen and (max-width: 35em) { - .card { - flex-direction: row; - padding: 1rem; - gap: 1rem; - border-radius: 12px; - } - - .avatar { - width: 48px; - height: 48px; - } - - .infoText h3 { - font-size: 1rem; - } - - .infoText p { - font-size: 0.8rem; - } - - .indexBadge span { - font-size: 0.8rem; - padding: 0.3rem 0.6rem; - } -} diff --git a/Components/Friend/Chat/Chat.jsx b/Components/Friend/Chat/Chat.jsx deleted file mode 100644 index ad655207..00000000 --- a/Components/Friend/Chat/Chat.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/router"; - -// INTERNAL IMPORT -import Style from "./Chat.module.css"; -import images from "../../../assets"; -import { converTime } from "../../../Utils/apiFeature"; -import { Loader } from "../../index"; - -const Chat = ({ - functionName, - readMessage, - friendMsg, - account, - userName, - loading, - currentUserName, - currentUserAddress, - readUser, -}) => { - const [message, setMessage] = useState(""); - const router = useRouter(); - // The open chat comes from the URL (?name=...&address=...) - const chatData = { - name: router.query.name || "", - address: router.query.address || "", - }; - - useEffect(() => { - if (chatData.address) { - readMessage(chatData.address); - readUser(chatData.address); - } - }, [chatData.address, readMessage, readUser]); - - const send = async () => { - const text = message.trim(); - if (!text) return; - await functionName({ msg: text, address: chatData.address }); - setMessage(""); - }; - - return ( -
- {/* User Info Header */} - {currentUserName && currentUserAddress && ( -
- User Avatar -
-

{currentUserName}

-

{currentUserAddress}

-
-
- )} - - {/* Chat Body */} -
-
-
- {friendMsg.length > 0 ? ( - friendMsg.map((el, i) => ( -
-
- Profile - - {el.sender === chatData.address - ? chatData.name - : userName} - Time: {converTime(el.timestamp)} - -
-

{el.msg}

-
- )) - ) : ( -

- No messages yet. Start chatting! -

- )} -
-
- - {/* Message Input Box */} - {currentUserName && currentUserAddress && ( -
-
- setMessage(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !loading) send(); - }} - /> - - {loading ? ( - - ) : ( - Send Message - )} -
-
- )} -
-
- ); -}; - -export default Chat; diff --git a/Components/Friend/Chat/Chat.module.css b/Components/Friend/Chat/Chat.module.css deleted file mode 100644 index 020d75ae..00000000 --- a/Components/Friend/Chat/Chat.module.css +++ /dev/null @@ -1,211 +0,0 @@ -.Chat { - width: 85%; - margin: 2rem auto; - padding: 1.5rem; - background: linear-gradient(145deg, rgba(41, 47, 63, 0.95), rgba(20, 25, 40, 0.95)); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - display: flex; - flex-direction: column; - gap: 1.5rem; - transition: all 0.3s ease-in-out; -} - -.Chat_user_info { - display: flex; - align-items: center; - gap: 1.5rem; - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; - padding: 1rem 1.5rem; - transition: background 0.3s ease-in-out; -} - -.Chat_user_info:hover { - background: rgba(255, 255, 255, 0.1); -} - -.Chat_user_info_img { - border-radius: 50%; - object-fit: cover; -} - -.Chat_user_info_box h4 { - color: #FFFFFF; - font-size: 1.3rem; - font-weight: 600; - margin: 0; -} - -.Chat_user_info_box p { - color: #B0B6C1; - font-size: 0.9rem; - margin: 0.3rem 0 0; -} - -/* Chat Body */ -.Chat_box_container { - background: rgba(255, 255, 255, 0.03); - border-radius: 12px; - padding: 1rem; - overflow-y: auto; - max-height: 65vh; -} - -.Chat_box { - display: flex; - flex-direction: column; - gap: 1.2rem; -} - -.Chat_box_left { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.Chat_message_block { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.Chat_box_left_title { - display: flex; - align-items: center; - gap: 1rem; -} - -.Chat_box_left_img { - border-radius: 50%; - object-fit: cover; - transition: transform 0.3s ease-in-out; -} - -.Chat_box_left_img:hover { - transform: scale(1.1); -} - -.Chat_box_left_title span { - display: flex; - flex-direction: column; - gap: 0.2rem; - color: #FFFFFF; - font-weight: 500; -} - -.Chat_box_left_title small { - color: #B0B6C1; - font-size: 0.85rem; -} - -.Chat_box_left p { - background: rgba(241, 131, 3, 0.15); - color: #FFFFFF; - padding: 1rem 1.3rem; - border-radius: 12px 12px 12px 0; - font-size: 1rem; - max-width: 70%; - word-wrap: break-word; - transition: background 0.3s ease-in-out; -} - -.Chat_box_left p:hover { - background: rgba(241, 131, 3, 0.25); -} - -.no_message { - color: #B0B6C1; - text-align: center; - margin-top: 2rem; - font-size: 1rem; -} - -/* Input Box */ -.Chat_box_send { - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; - padding: 1rem; -} - -.Chat_box_send_img { - display: flex; - align-items: center; - gap: 1rem; -} - -.Chat_box_send_img input { - flex: 1; - background: rgba(241, 131, 3, 0.1); - outline: none; - border: none; - padding: 0.8rem 1.2rem; - color: #FFFFFF; - font-size: 1rem; - border-radius: 8px; - transition: background 0.3s ease-in-out; -} - -.Chat_box_send_img input::placeholder { - color: #B0B6C1; -} - -.Chat_box_send_img input:focus { - background: rgba(241, 131, 3, 0.2); -} - -.send_icon { - cursor: pointer; - transition: transform 0.3s ease-in-out; -} - -.send_icon:hover { - transform: scale(1.1); -} - -/* Responsive Design */ -@media only screen and (max-width: 35em) { - .Chat { - width: 90%; - padding: 1rem; - } - - .Chat_user_info { - gap: 1rem; - padding: 0.8rem; - } - - .Chat_user_info_img { - width: 50px; - height: 50px; - } - - .Chat_user_info_box h4 { - font-size: 1.1rem; - } - - .Chat_box_left p { - font-size: 0.9rem; - max-width: 85%; - padding: 0.8rem 1.1rem; - } - - .Chat_box_left_title img { - width: 35px; - height: 35px; - } - - .Chat_box_send_img input { - font-size: 0.9rem; - padding: 0.6rem 1rem; - } - - .send_icon { - width: 35px; - height: 35px; - } - - .show { - display: none; - } -} diff --git a/Components/Friend/Friend.jsx b/Components/Friend/Friend.jsx deleted file mode 100644 index 7c7600b2..00000000 --- a/Components/Friend/Friend.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useContext } from "react"; -import Image from "next/image"; - -// INTERNAL IMPORT -import Style from "./Friend.module.css"; -import images from "../../assets"; -import Card from "./Card/Card"; -import Chat from "./Chat/Chat"; -import { ChatAppContect } from "../../Context/ChatAppContext"; - -const Friend = () => { - const { - sendMessage, - account, - friendLists, - readMessage, - userName, - loading, - friendMsg, - currentUserName, - currentUserAddress, - readUser, - } = useContext(ChatAppContect); - - return ( -
-
- {/* Left Side - Friend List */} -
-

Friends

- {friendLists && friendLists.length > 0 ? ( - friendLists.map((el, i) => ( - - )) - ) : ( -

No friends added yet.

- )} -
- - {/* Right Side - Chat Section */} -
- -
-
-
- ); -}; - -export default Friend; diff --git a/Components/Friend/Friend.module.css b/Components/Friend/Friend.module.css deleted file mode 100644 index 48830d7d..00000000 --- a/Components/Friend/Friend.module.css +++ /dev/null @@ -1,101 +0,0 @@ -.Friend { - width: 85%; - margin: 2rem auto; - padding: 1.5rem; - background: linear-gradient(145deg, rgba(41, 47, 63, 0.95), rgba(20, 25, 40, 0.95)); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: all 0.3s ease-in-out; -} - -.Friend_box { - display: grid; - grid-template-columns: 1.2fr 2fr; - gap: 2rem; -} - -.section_title { - color: #FFFFFF; - font-size: 1.4rem; - font-weight: 600; - margin-bottom: 1.2rem; - letter-spacing: 0.5px; -} - -.Friend_box_left { - background: rgba(255, 255, 255, 0.05); - padding: 1.5rem; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - max-height: 80vh; - overflow-y: auto; - transition: all 0.3s ease-in-out; -} - -.Friend_box_left:hover { - background: rgba(255, 255, 255, 0.07); - box-shadow: 0 4px 14px rgba(241, 131, 3, 0.2); -} - -.no_friends { - color: #B0B6C1; - text-align: center; - font-size: 1rem; - margin-top: 2rem; - opacity: 0.8; -} - -.Friend_box_right { - background: rgba(255, 255, 255, 0.05); - padding: 1.5rem; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - transition: all 0.3s ease-in-out; -} - -.Friend_box_right:hover { - background: rgba(255, 255, 255, 0.07); - box-shadow: 0 4px 14px rgba(241, 131, 3, 0.2); -} - -/* Scrollbar Styling */ -.Friend_box_left::-webkit-scrollbar { - width: 6px; -} - -.Friend_box_left::-webkit-scrollbar-thumb { - background: rgba(241, 131, 3, 0.5); - border-radius: 10px; -} - -.Friend_box_left::-webkit-scrollbar-thumb:hover { - background: rgba(241, 131, 3, 0.7); -} - -/* Responsive Design */ -@media only screen and (max-width: 35em) { - .Friend { - width: 90%; - margin: 1.5rem auto; - padding: 1rem; - } - - .Friend_box { - grid-template-columns: 1fr; - gap: 1.5rem; - } - - .Friend_box_left { - padding: 1rem; - max-height: 50vh; - } - - .Friend_box_right { - padding: 1rem; - } - - .section_title { - font-size: 1.2rem; - text-align: center; - } -} diff --git a/Components/Loader/Loader.jsx b/Components/Loader/Loader.jsx deleted file mode 100644 index e2061afb..00000000 --- a/Components/Loader/Loader.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import Image from "next/image"; -import Style from "./Loader.module.css"; -import images from "../../assets"; - -const Loader = () => { - return ( -
-
-
-
- Loading... -
-

Please wait, loading your chat...

-
-
- ); -}; - -export default Loader; diff --git a/Components/Loader/Loader.module.css b/Components/Loader/Loader.module.css deleted file mode 100644 index b0a07703..00000000 --- a/Components/Loader/Loader.module.css +++ /dev/null @@ -1,118 +0,0 @@ -.Loader { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: radial-gradient(circle at top left, #1a1f35, #0d111a); - backdrop-filter: blur(12px); - z-index: 9999; - animation: fadeIn 0.5s ease-in-out forwards; -} - -.Loader_box { - position: relative; - text-align: center; - background: rgba(255, 255, 255, 0.08); - border-radius: 20px; - padding: 3rem 2.5rem; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.Loader_glow { - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: conic-gradient( - from 0deg, - #f18303, - #ff9b4a, - #f18303, - #ffb966, - #f18303 - ); - animation: rotateGlow 3s linear infinite; - filter: blur(60px); - opacity: 0.3; - z-index: 0; -} - -.Loader_spinner { - position: relative; - z-index: 1; - display: inline-block; - animation: spinPulse 2s ease-in-out infinite; -} - -.Loader_img { - width: 90px; - height: 90px; - animation: spin 1.8s linear infinite; - filter: drop-shadow(0 0 8px rgba(241, 131, 3, 0.8)); -} - -.Loader_text { - margin-top: 1.5rem; - font-size: 1.1rem; - color: #fff; - font-weight: 500; - letter-spacing: 0.5px; - opacity: 0.9; - z-index: 2; - position: relative; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -@keyframes spinPulse { - 0%, 100% { - transform: scale(1); - } - 50% { - transform: scale(1.1); - } -} - -@keyframes rotateGlow { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@media only screen and (max-width: 35em) { - .Loader_box { - padding: 2rem; - } - - .Loader_img { - width: 65px; - height: 65px; - } - - .Loader_text { - font-size: 1rem; - } -} diff --git a/Components/Model/Model.jsx b/Components/Model/Model.jsx deleted file mode 100644 index 81c403f2..00000000 --- a/Components/Model/Model.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState, useContext } from "react"; -import Image from "next/image"; - -// INTERNAL IMPORT -import Style from "./Model.module.css"; -import images from "../../assets"; -import { ChatAppContect } from "../../Context/ChatAppContext"; -import { Loader } from "../../Components/index"; - -const Model = ({ - openBox, - title, - address, - head, - info, - smallInfo, - image, - functionName, -}) => { - const [name, setName] = useState(""); - const [userAddress, setUserAddress] = useState(address || ""); - - const { loading } = useContext(ChatAppContect); - - return ( -
-
-
- buddy -
- -
-

- {title} {head} -

-

{info}

- {smallInfo} - - {loading ? ( - - ) : ( -
- {/* Name Input */} -
- user - setName(e.target.value)} - /> -
- - {/* Address Input */} -
- user - setUserAddress(e.target.value)} - /> -
- - {/* Action Buttons */} -
- - - -
-
- )} -
-
-
- ); -}; - -export default Model; diff --git a/Components/Model/Model.module.css b/Components/Model/Model.module.css deleted file mode 100644 index 238dc5f2..00000000 --- a/Components/Model/Model.module.css +++ /dev/null @@ -1,337 +0,0 @@ -.Model { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(20, 20, 30, 0.98); - backdrop-filter: blur(20px); - z-index: 10000; - animation: fadeIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; -} - -.Model_box { - width: 90%; - max-width: 950px; - display: grid; - grid-template-columns: 1.6fr 1fr; - align-items: start; - background: linear-gradient(145deg, rgba(30, 30, 45, 0.95), rgba(50, 50, 70, 0.95)); - border-radius: 24px; - padding: 2.5rem; - border: 1px solid rgba(100, 150, 255, 0.3); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.5), - inset 0 1px 0 rgba(255, 255, 255, 0.1); - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - position: relative; - overflow: hidden; -} - -.Model_box::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(100, 150, 255, 0.5), transparent); -} - -.Model_box:hover { - transform: translateY(-8px) scale(1.02); - box-shadow: - 0 25px 50px rgba(100, 150, 255, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.15); -} - -.Model_box_left_img { - width: 100%; - height: auto; - max-height: 550px; - object-fit: cover; - border-radius: 20px; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.3)); -} - -.Model_box_left_img:hover { - transform: scale(1.08) rotate(1deg); - filter: drop-shadow(0 15px 30px rgba(100, 150, 255, 0.4)); -} - -.Model_box_right { - display: flex; - flex-direction: column; - gap: 2rem; - padding-left: 2rem; -} - -.Model_box_right h1 { - color: #ffffff; - font-size: 2.8rem; - font-weight: 800; - margin: 0; - letter-spacing: -0.02em; - line-height: 1.1; -} - -.Model_box_right h1 span { - display: block; - background: linear-gradient(135deg, #64b5f6, #42a5f5); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - font-size: 3.5rem; - font-weight: 900; - text-shadow: 0 0 20px rgba(100, 150, 255, 0.5); -} - -.Model_box_right p { - color: #d1d5db; - font-size: 1.2rem; - margin: 0; - line-height: 1.6; - font-weight: 300; -} - -.Model_box_right small { - color: #9ca3af; - font-size: 1rem; - font-style: italic; - letter-spacing: 0.5px; -} - -.Model_box_right_name { - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -.Model_box_right_name_info { - display: flex; - align-items: center; - gap: 1rem; - background: linear-gradient(145deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04)); - padding: 1rem 1.5rem; - border-radius: 16px; - border: 1px solid rgba(100, 150, 255, 0.2); - transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); - position: relative; - overflow: hidden; -} - -.Model_box_right_name_info::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, transparent, rgba(100, 150, 255, 0.05), transparent); - opacity: 0; - transition: opacity 0.3s ease; - pointer-events: none; -} - -.Model_box_right_name_info:hover { - background: linear-gradient(145deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06)); - border-color: rgba(100, 150, 255, 0.4); - transform: translateX(4px); -} - -.Model_box_right_name_info:hover::before { - opacity: 1; -} - -.Model_box_right_name_info img { - width: 32px; - height: 32px; - transition: all 0.3s ease; - filter: brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); - flex-shrink: 0; -} - -.Model_box_right_name_info img:hover { - transform: scale(1.15) rotate(10deg); - filter: brightness(0) invert(1) drop-shadow(0 4px 8px rgba(100, 150, 255, 0.3)); -} - -.Model_box_right_name_info input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: #ffffff; - font-size: 1.1rem; - font-weight: 400; - letter-spacing: 0.5px; - cursor: text; - padding: 0.25rem 0; -} - -.Model_box_right_name_info input::placeholder { - color: #9ca3af; - font-weight: 300; -} - -.Model_box_right_name_info input:focus { - color: #64b5f6; - text-shadow: 0 0 10px rgba(100, 150, 255, 0.5); - outline: 2px solid rgba(100, 150, 255, 0.5); - border-radius: 4px; -} - -.Model_box_right_name_btn { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1.25rem; -} - -.submit_btn, -.cancel_btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border: none; - padding: 1rem 2rem; - color: #ffffff; - font-size: 1.1rem; - font-weight: 600; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - gap: 0.75rem; - cursor: pointer; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - box-shadow: - 0 8px 20px rgba(102, 126, 234, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.2); - position: relative; - overflow: hidden; -} - -.cancel_btn { - background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); -} - -.submit_btn::after, -.cancel_btn::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s; -} - -.submit_btn:hover::after, -.cancel_btn:hover::after { - left: 100%; -} - -.submit_btn:hover { - background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); - transform: translateY(-4px); - box-shadow: - 0 12px 25px rgba(102, 126, 234, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.3); -} - -.cancel_btn:hover { - background: linear-gradient(135deg, #ee5a52 0%, #ff6b6b 100%); - transform: translateY(-4px); - box-shadow: - 0 12px 25px rgba(255, 107, 107, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.3); -} - -.submit_btn img, -.cancel_btn img { - filter: brightness(0) invert(1); - transition: all 0.3s ease; - flex-shrink: 0; -} - -.submit_btn:hover img, -.cancel_btn:hover img { - filter: brightness(0) invert(1) drop-shadow(0 2px 4px rgba(255, 255, 255, 0.3)); -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@media only screen and (max-width: 35em) { - .Model_box { - grid-template-columns: 1fr; - padding: 2rem; - width: 95%; - gap: 1.5rem; - } - - .Model_box_left_img { - max-height: 350px; - margin-bottom: 0; - } - - .Model_box_right { - padding-left: 0; - gap: 1.5rem; - } - - .Model_box_right h1 { - font-size: 2.2rem; - } - - .Model_box_right h1 span { - font-size: 2.8rem; - } - - .Model_box_right p { - font-size: 1.1rem; - } - - .Model_box_right small { - font-size: 0.95rem; - } - - .Model_box_right_name_info { - padding: 0.9rem 1.25rem; - gap: 0.75rem; - } - - .Model_box_right_name_info input { - font-size: 1rem; - } - - .Model_box_right_name_info img { - width: 28px; - height: 28px; - } - - .Model_box_right_name_btn { - grid-template-columns: 1fr; - gap: 1rem; - } - - .submit_btn, - .cancel_btn { - font-size: 1rem; - padding: 0.9rem 1.5rem; - } - - .submit_btn img, - .cancel_btn img { - width: 24px; - height: 24px; - } -} \ No newline at end of file diff --git a/Components/NavBar.jsx b/Components/NavBar.jsx new file mode 100644 index 00000000..7fd28325 --- /dev/null +++ b/Components/NavBar.jsx @@ -0,0 +1,104 @@ +import React, { useState, useContext } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +import { ChatAppContext } from "../Context/ChatAppContext"; +import { shortAddress } from "../Utils/apiFeature"; +import Avatar from "./Avatar"; +import AccountDialog from "./AccountDialog"; +import Notice from "./Notice"; +import Spinner from "./Spinner"; + +const links = [ + { label: "Chat", href: "/" }, + { label: "Find people", href: "/alluser" }, + { label: "Leaderboard", href: "/leaderboard" }, +]; + +const Mark = () => ( + +); + +const NavBar = () => { + const [creating, setCreating] = useState(false); + const { pathname } = useRouter(); + const { + account, + userName, + connectWallet, + connecting, + error, + clearError, + tokenBalance, + } = useContext(ChatAppContext); + + return ( +
+
+ + + Node Connect + + + + +
+ {!account ? ( + + ) : ( + <> + + {tokenBalance} TOK + + {userName ? ( + + + {userName} + + ) : ( + + )} + + {shortAddress(account)} + + + )} +
+
+ + {creating && setCreating(false)} />} + {error && } +
+ ); +}; + +export default NavBar; diff --git a/Components/NavBar/NavBar.jsx b/Components/NavBar/NavBar.jsx deleted file mode 100644 index c4d9fefb..00000000 --- a/Components/NavBar/NavBar.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useEffect, useState, useContext } from "react"; -import Image from "next/image"; -import Link from "next/link"; - -//INTERNAL IMPORT -import Style from "./NavBar.module.css"; -import { ChatAppContect } from "../../Context/ChatAppContext"; -import { Model, Error } from "../index"; -import images from "../../assets"; - -const NavBar = () => { - const menuItems = [ - { - menu: "Add Friends", - link: "alluser", - }, - { - menu: "Chat", - link: "/", - }, - { - menu: "LeaderBoard", - link: "leaderboard", - }, - ]; - - //USESTATE - const [active, setActive] = useState(2); - const [open, setOpen] = useState(false); - const [openModel, setOpenModel] = useState(false); - - const { - account, - userName, - connectWallet, - createAccount, - error, - clearError, - tokenBalance, - } = useContext(ChatAppContect); - return ( -
-
-
- logo -
-
- {/* //DESKTOP */} -
- {menuItems.map((el, i) => ( -
setActive(i + 1)} - key={i + 1} - className={`${Style.NavBar_box_right_menu_items} ${ - active == i + 1 ? Style.active_btn : "" - }`} - > - - {el.menu} - -
- ))} -
- - {/* //MOBILE */} - {open && ( -
- {menuItems.map((el, i) => ( -
setActive(i + 1)} - key={i + 1} - className={`${Style.mobile_menu_items} ${ - active == i + 1 ? Style.active_btn : "" - }`} - > - - {el.menu} - -
- ))} - -

- close setOpen(false)} - /> -

-
- )} - - {/* CONNECT WALLET */} -
- {account === "" ? ( - - ) : ( -
-
- - -
- {tokenBalance} TOK -
-
-
- )} -
- -
setOpen(true)} - > - open -
-
-
- - {/* MODEL COMPONENT */} - {openModel && ( - - )} - {error && } -
- ); -}; - -export default NavBar; diff --git a/Components/NavBar/NavBar.module.css b/Components/NavBar/NavBar.module.css deleted file mode 100644 index 70d7441f..00000000 --- a/Components/NavBar/NavBar.module.css +++ /dev/null @@ -1,217 +0,0 @@ -.NavBar { - width: 85%; - margin: 2rem auto; - position: relative; - background: linear-gradient(145deg, rgba(41, 47, 63, 0.95), rgba(20, 25, 40, 0.95)); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - padding: 1rem; -} - -.NavBar_box { - display: grid; - grid-template-columns: 1fr 2fr; - align-items: center; - gap: 2rem; -} - -.NavBar_box_right { - display: flex; - align-items: center; - justify-content: space-between; -} - -.NavBar_box_right_menu { - display: flex; - align-items: center; - gap: 2.5rem; - transition: all 0.3s ease-in-out; -} - -.NavBar_box_right_menu_items { - position: relative; - padding: 0.5rem 0; - transition: all 0.3s ease-in-out; -} - -.NavBar_box_right_menu_items_link { - color: #FFFFFF; - font-weight: 500; - font-size: 1.1rem; - text-transform: uppercase; - letter-spacing: 0.5px; - transition: color 0.3s ease-in-out; -} - -.NavBar_box_right_menu_items_link:hover { - color: #F18303; -} - -.active_btn { - color: #F18303; - border-bottom: 2px solid #F18303; - padding-bottom: 0.4rem; -} - -.NavBar_box_right_connect { - display: flex; - justify-self: flex-end; -} - -.NavBar_box_right_connect button { - background: linear-gradient(90deg, #F18303, #FFAB38); - padding: 0.8rem 1.5rem; - border: none; - border-radius: 8px; - font-size: 1rem; - color: #FFFFFF; - font-weight: 600; - cursor: pointer; - display: flex; - align-items: center; - gap: 0.5rem; - transition: all 0.3s ease-in-out; - box-shadow: 0 2px 8px rgba(241, 131, 3, 0.3); -} - -.NavBar_box_right_connect button:hover { - background: linear-gradient(90deg, #FFAB38, #F18303); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(241, 131, 3, 0.5); -} - -.NavBar_box_right_close { - display: none; -} - -.NavBar_box_right_open { - display: none; -} - -.mobile_menu { - display: none; -} - -.modelBox { - position: fixed; - inset: 0; - background: rgba(41, 47, 63, 0.95); - z-index: 77777777; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(8px); -} - -.connectedSection { - display: flex; - align-items: center; -} - -.accountTokenWrapper { - display: flex; - align-items: center; - gap: 1rem; /* space between user and token balance */ -} - -.accountBtn { - display: flex; - align-items: center; - gap: 0.5rem; - background: linear-gradient(90deg, #F18303, #FFAB38); - border: none; - padding: 0.5rem 1rem; - border-radius: 8px; - color: #fff; - font-weight: 600; - cursor: pointer; - transition: all 0.3s ease-in-out; -} - -.accountBtn:hover { - background: linear-gradient(90deg, #FFAB38, #F18303); - transform: translateY(-2px); -} - -.tokenBalance { - background: rgba(255, 255, 255, 0.05); - padding: 0.4rem 0.8rem; - border-radius: 8px; - font-weight: 600; - color: #f18303; - box-shadow: 0 2px 8px rgba(241, 131, 3, 0.3); - transition: all 0.3s ease-in-out; -} - -.tokenBalance:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(241, 131, 3, 0.45); -} - - -@media only screen and (max-width: 35em) { - .NavBar { - width: 90%; - margin: 1.5rem auto; - padding: 0.8rem; - } - - .NavBar_box { - grid-template-columns: 0.5fr 2fr; - justify-content: space-between; - } - - .NavBar_box_right_connect button { - font-size: 0.9rem; - padding: 0.6rem 1rem; - } - - .NavBar_box_right_open { - display: block; - cursor: pointer; - } - - .NavBar_box_right_menu { - display: none; - } - - .mobile_menu { - display: block; - position: fixed; - z-index: 1111111; - background: rgba(41, 47, 63, 0.95); - inset: 0; - text-align: center; - padding: 4rem 2rem; - backdrop-filter: blur(8px); - } - - .mobile_menu_items { - margin: 1.5rem 0; - padding: 0.5rem 0; - } - - .mobile_menu_items_link { - color: #FFFFFF; - font-size: 1.2rem; - font-weight: 500; - text-transform: uppercase; - transition: color 0.3s ease-in-out; - } - - .mobile_menu_items_link:hover { - color: #F18303; - } - - .mobile_menu_btn { - padding-top: 2rem; - cursor: pointer; - } - - .modelBox { - position: absolute; - inset: 0 0 -70% 0; - z-index: 11111; - background: rgba(41, 47, 63, 0.95); - } -} \ No newline at end of file diff --git a/Components/Notice.jsx b/Components/Notice.jsx new file mode 100644 index 00000000..0dbed354 --- /dev/null +++ b/Components/Notice.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +const Notice = ({ message, onClose }) => ( +
+

{message}

+ +
+); + +export default Notice; diff --git a/Components/Spinner.jsx b/Components/Spinner.jsx new file mode 100644 index 00000000..0e05d6af --- /dev/null +++ b/Components/Spinner.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +const Spinner = () => ; + +export default Spinner; diff --git a/Components/UserCard/UserCard.jsx b/Components/UserCard/UserCard.jsx deleted file mode 100644 index 63033a18..00000000 --- a/Components/UserCard/UserCard.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from "react"; -import Image from "next/image"; - -// INTERNAL IMPORT -import Style from "./UserCard.module.css"; -import images from "../../assets"; - -const UserCard = ({ el, i, addFriends }) => { - return ( -
-
- {/* User Image */} -
- user -
- - {/* User Info */} -
-
-

{el.name}

- {/* Future token display can go here */} -
-

{el.accountAddress.slice(0, 25)}..

- -
-
- - {/* Card Index */} - {i + 1} -
- ); -}; - -export default UserCard; diff --git a/Components/UserCard/UserCard.module.css b/Components/UserCard/UserCard.module.css deleted file mode 100644 index 7ea60d36..00000000 --- a/Components/UserCard/UserCard.module.css +++ /dev/null @@ -1,211 +0,0 @@ -.UserCard { - background: linear-gradient(145deg, rgba(30, 30, 45, 0.95), rgba(50, 50, 70, 0.95)); - border-radius: 20px; - text-align: center; - padding: 2rem; - position: relative; - box-shadow: - 0 12px 30px rgba(0, 0, 0, 0.5), - inset 0 1px 0 rgba(255, 255, 255, 0.1); - border: 1px solid rgba(241, 131, 3, 0.2); - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - cursor: pointer; - overflow: hidden; -} - -.UserCard::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(241, 131, 3, 0.5), transparent); -} - -.UserCard:hover { - transform: translateY(-8px) scale(1.03); - box-shadow: - 0 20px 40px rgba(241, 131, 3, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.15); - background: linear-gradient(145deg, rgba(30, 30, 45, 1), rgba(50, 50, 70, 1)); -} - -.UserCard_box { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.5rem; -} - -.UserCard_box_img_wrapper { - width: 110px; - height: 110px; - border-radius: 50%; - padding: 6px; - background: linear-gradient(135deg, #f18303, #ffab38); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - box-shadow: 0 8px 20px rgba(241, 131, 3, 0.3); - position: relative; - overflow: hidden; -} - -.UserCard_box_img_wrapper::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, rgba(241, 131, 3, 0.2), transparent); - opacity: 0; - transition: opacity 0.3s ease; -} - -.UserCard_box_img_wrapper:hover { - transform: scale(1.15) rotate(5deg); -} - -.UserCard_box_img_wrapper:hover::after { - opacity: 1; -} - -.UserCard_box_img { - border-radius: 50%; - object-fit: cover; - filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); - transition: all 0.3s ease; -} - -.UserCard_box_img:hover { - filter: drop-shadow(0 6px 12px rgba(241, 131, 3, 0.5)); -} - -.UserCard_box_info { - display: flex; - flex-direction: column; - gap: 0.75rem; - align-items: center; -} - -.UserCard_box_name { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.UserCard_box_info h3 { - color: #ffffff; - font-size: 1.4rem; - font-weight: 800; - margin: 0; - letter-spacing: -0.01em; - line-height: 1.2; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -} - -.UserCard_box_info p { - color: #d1d5db; - font-size: 0.95rem; - margin: 0; - word-break: break-all; - font-family: monospace; - letter-spacing: 0.5px; - opacity: 0.8; -} - -.UserCard_box_info button { - background: linear-gradient(135deg, #f18303, #ffab38); - color: #ffffff; - width: 100%; - max-width: 180px; - padding: 1rem 1.8rem; - border: none; - border-radius: 12px; - font-size: 1.05rem; - font-weight: 600; - cursor: pointer; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - box-shadow: - 0 8px 20px rgba(241, 131, 3, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.2); - position: relative; - overflow: hidden; -} - -.UserCard_box_info button::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s; -} - -.UserCard_box_info button:hover::after { - left: 100%; -} - -.UserCard_box_info button:hover { - background: linear-gradient(135deg, #ffab38, #f18303); - transform: translateY(-3px); - box-shadow: - 0 12px 25px rgba(241, 131, 3, 0.6), - inset 0 1px 0 rgba(255, 255, 255, 0.3); -} - -.number { - position: absolute; - right: 1.2rem; - top: 1.2rem; - padding: 0.6rem 1rem; - border-radius: 50%; - background: linear-gradient(135deg, rgba(241, 131, 3, 0.2), rgba(255, 171, 56, 0.2)); - color: #f18303; - font-size: 1rem; - font-weight: 700; - transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); - box-shadow: 0 4px 12px rgba(241, 131, 3, 0.3); - border: 1px solid rgba(241, 131, 3, 0.3); -} - -.number:hover { - transform: scale(1.25) rotate(10deg); - background: linear-gradient(135deg, rgba(241, 131, 3, 0.4), rgba(255, 171, 56, 0.4)); - box-shadow: 0 6px 16px rgba(241, 131, 3, 0.5); -} - -@media only screen and (max-width: 600px) { - .UserCard { - padding: 1.5rem; - } - - .UserCard_box_img_wrapper { - width: 80px; - height: 80px; - padding: 4px; - } - - .UserCard_box_info h3 { - font-size: 1.2rem; - } - - .UserCard_box_info p { - font-size: 0.85rem; - } - - .UserCard_box_info button { - font-size: 0.95rem; - padding: 0.8rem 1.4rem; - max-width: 150px; - } - - .number { - padding: 0.5rem 0.8rem; - font-size: 0.9rem; - right: 1rem; - top: 1rem; - } -} \ No newline at end of file diff --git a/Components/index.js b/Components/index.js index 9a14b1ec..3947c41e 100644 --- a/Components/index.js +++ b/Components/index.js @@ -1,9 +1,8 @@ -import NavBar from "./NavBar/NavBar"; -import Filter from "./Filter/Filter"; -import Error from "./Error/Error"; -import Loader from "./Loader/Loader"; -import Model from "./Model/Model"; -import UserCard from "./UserCard/UserCard"; -import Friend from "./Friend/Friend"; +import NavBar from "./NavBar"; +import Avatar from "./Avatar"; +import AccountDialog from "./AccountDialog"; +import EmptyState from "./EmptyState"; +import Notice from "./Notice"; +import Spinner from "./Spinner"; -export { NavBar, Filter, Error, Loader, Model, UserCard, Friend }; +export { NavBar, Avatar, AccountDialog, EmptyState, Notice, Spinner }; diff --git a/Context/ChatAppContext.js b/Context/ChatAppContext.js index d9c8ff62..284319ff 100644 --- a/Context/ChatAppContext.js +++ b/Context/ChatAppContext.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { useRouter } from "next/router"; import { @@ -12,24 +12,24 @@ import { errorMessage, } from "../Utils/apiFeature"; -export const ChatAppContect = React.createContext(); +export const ChatAppContext = React.createContext(); const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase(); export const ChatAppProvider = ({ children }) => { const [account, setAccount] = useState(""); const [userName, setUserName] = useState(""); + const [ready, setReady] = useState(false); + const [connecting, setConnecting] = useState(false); + const [busy, setBusy] = useState(""); const [friendLists, setFriendLists] = useState([]); const [friendMsg, setFriendMsg] = useState([]); - const [loading, setLoading] = useState(false); + const [pendingMsg, setPendingMsg] = useState([]); const [userLists, setUserLists] = useState([]); const [userListsforleaderboard, setUserListsforleaderboard] = useState([]); const [tokenBalance, setTokenBalance] = useState(0); const [error, setError] = useState(""); - - // The friend whose chat is open - const [currentUserName, setCurrentUserName] = useState(""); - const [currentUserAddress, setCurrentUserAddress] = useState(""); + const connectingRef = useRef(false); const router = useRouter(); @@ -77,7 +77,8 @@ export const ChatAppProvider = ({ children }) => { setAccount(address); await loadAccount(address); }) - .catch((err) => setError(errorMessage(err))); + .catch((err) => setError(errorMessage(err))) + .finally(() => setReady(true)); if (!hasWallet()) return undefined; // Reload the data when the user switches account in MetaMask @@ -87,7 +88,12 @@ export const ChatAppProvider = ({ children }) => { window.ethereum.removeListener?.("accountsChanged", onAccountsChanged); }, [loadAccount]); + // One MetaMask prompt at a time: a second request while the first is + // open is rejected by MetaMask (error -32002). const connectWallet = async () => { + if (connectingRef.current) return; + connectingRef.current = true; + setConnecting(true); try { const address = await requestWallet(); if (!address) return; @@ -95,21 +101,27 @@ export const ChatAppProvider = ({ children }) => { await loadAccount(address); } catch (err) { setError(errorMessage(err)); + } finally { + connectingRef.current = false; + setConnecting(false); } }; - // Sends a transaction, waits for it to be mined, then runs `after` - const runTransaction = async (send, after) => { + // Sends a transaction and waits for it to be mined. Returns true when it + // went through, so the caller can keep its input when it did not. + const runTransaction = async (label, send, after) => { + setBusy(label); try { const contract = await connectingWithContract(); const tx = await send(contract); - setLoading(true); await tx.wait(); - setLoading(false); await after(); + return true; } catch (err) { - setLoading(false); setError(errorMessage(err)); + return false; + } finally { + setBusy(""); } }; @@ -124,70 +136,75 @@ export const ChatAppProvider = ({ children }) => { }, []); const createAccount = async ({ name }) => { - if (!name?.trim() || !account) - return setError("Name and account address cannot be empty"); - await runTransaction( + if (!name?.trim() || !account) { + setError("Choose a name first."); + return false; + } + return runTransaction( + "account", (contract) => contract.createAccount(name.trim()), () => loadAccount(account) ); }; const addFriends = async ({ name, userAddress }) => { - if (!name || !userAddress) return setError("Please provide a name and address"); - await runTransaction( + if (!name || !userAddress) { + setError("A name and an address are needed to add a friend."); + return false; + } + return runTransaction( + `friend:${userAddress}`, (contract) => contract.addFriend(userAddress, name), async () => { await loadAccount(account); - router.push("/"); + router.push({ pathname: "/", query: { name, address: userAddress } }); } ); }; + // The message shows at once as pending and is replaced by the stored one + // after the chain confirms it. const sendMessage = async ({ msg, address }) => { - if (!msg?.trim() || !address) return setError("Please type your message"); - await runTransaction( - (contract) => contract.sendMessage(address, msg), + const text = msg?.trim(); + if (!text || !address) return false; + const pending = { id: Date.now(), to: address, sender: account, msg: text }; + setPendingMsg((list) => [...list, pending]); + const sent = await runTransaction( + "message", + (contract) => contract.sendMessage(address, text), async () => { await readMessage(address); await loadAccount(account); } ); + setPendingMsg((list) => list.filter((m) => m.id !== pending.id)); + return sent; }; - const readUser = useCallback(async (userAddress) => { - try { - const contract = await connectingWithContract(); - setCurrentUserName(await contract.getUsername(userAddress)); - setCurrentUserAddress(userAddress); - } catch (err) { - setError(errorMessage(err)); - } - }, []); - return ( - setError(""), - currentUserName, - currentUserAddress, userListsforleaderboard, tokenBalance, }} > {children} - + ); }; diff --git a/README.md b/README.md index 4d51c471..61fd7858 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@

- Chat screen: friends list on the left, the chat with bob on the right, the account name and a 13 TOK balance in the header + Chat screen: friends list on the left, the chat with bob on the right, the account name and an 11 TOK balance in the header

@@ -84,19 +84,19 @@ Things to try in the first two minutes: 1. **Connect Wallet** and approve the network switch: the app adds Hoodi to MetaMask if it is missing. 2. **Create an account** with a username: the header balance shows 5 TOK once the transaction is mined. -3. Open **Add Friends** and add someone: both of you get 1 TOK, and they appear in your friends list. +3. Open **Find people** and add someone: both of you get 1 TOK, and they appear in your friends list. 4. Open the chat with that friend and **send a message** (Enter sends): both balances go up by 1. 5. Open the **Leaderboard** to see everyone ranked by tokens. - - + +
Add Friends page: cards for dave and erin, each with an Add Friend button

Registered users you can add, read from the contract (how friendships work)

Leaderboard: alice 13, bob 12, carol 8, dave 7, erin 5 tokens

Everyone ranked by tokens (the reward rules)

Find people page: dave and erin, each with an Add friend button

Registered users you can add, read from the contract (how friendships work)

Leaderboard: alice 11, bob 10, carol 6, dave 5, erin 5 tokens

Everyone ranked by tokens (the reward rules)

Screenshots: the production build of this commit on a local Hardhat chain seeded with demo -accounts (alice, bob, carol, dave, erin), 1440x900, 2026-10-16. The live site shows the same +accounts (alice, bob, carol, dave, erin), 1440x900, 2026-10-21. The live site shows the same screens for Hoodi accounts. ## Features diff --git a/Utils/apiFeature.js b/Utils/apiFeature.js index 4f09dfd2..d1da04ef 100644 --- a/Utils/apiFeature.js +++ b/Utils/apiFeature.js @@ -44,25 +44,48 @@ export const toMessage = (m) => ({ msg: m.msg, }); -// Pulls the revert reason or MetaMask message out of an ethers error. -export const errorMessage = (error) => - error?.reason || - error?.info?.error?.message || - error?.shortMessage || - error?.message || - "Something went wrong"; +// MetaMask and ethers report the same user action under different codes. +const errorCode = (error) => error?.code ?? error?.info?.error?.code; -export const converTime = (seconds) => { - const date = new Date(Number(seconds) * 1000); +// Turns a wallet or contract error into one sentence a person can act on. +export const errorMessage = (error) => { + const code = errorCode(error); + if (code === 4001 || code === "ACTION_REJECTED") { + return "You declined the request in MetaMask. Nothing was changed."; + } + if (code === -32002) { + return "MetaMask already has a request waiting. Open the MetaMask extension, approve or reject it, then try again."; + } + if (code === "INSUFFICIENT_FUNDS") { + return "This account has no test ETH to pay the network fee."; + } + return ( + error?.reason || + error?.info?.error?.message || + error?.shortMessage || + error?.message || + "Something went wrong" + ); +}; - let hours = date.getHours(); - const minutes = date.getMinutes().toString().padStart(2, "0"); - const ampm = hours >= 12 ? "PM" : "AM"; - hours = hours % 12 || 12; +export const shortAddress = (address) => + address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ""; - const day = date.getDate().toString().padStart(2, "0"); - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - const year = date.getFullYear(); +const timeFormat = new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", +}); +const dayFormat = new Intl.DateTimeFormat(undefined, { + day: "numeric", + month: "short", + year: "numeric", +}); - return `${hours}:${minutes} ${ampm} Date: ${day}/${month}/${year}`; +// "3:42 PM" for today, "12 Oct 2026, 3:42 PM" for any other day. +export const formatTime = (seconds) => { + const date = new Date(Number(seconds) * 1000); + const time = timeFormat.format(date); + return date.toDateString() === new Date().toDateString() + ? time + : `${dayFormat.format(date)}, ${time}`; }; diff --git a/assets/account.png b/assets/account.png deleted file mode 100644 index 414f8f11..00000000 Binary files a/assets/account.png and /dev/null differ diff --git a/assets/acountName.png b/assets/acountName.png deleted file mode 100644 index 89862257..00000000 Binary files a/assets/acountName.png and /dev/null differ diff --git a/assets/avatar.gif b/assets/avatar.gif deleted file mode 100644 index b499cd5d..00000000 Binary files a/assets/avatar.gif and /dev/null differ diff --git a/assets/buddy.png b/assets/buddy.png deleted file mode 100644 index 2d4c54ce..00000000 Binary files a/assets/buddy.png and /dev/null differ diff --git a/assets/close.png b/assets/close.png deleted file mode 100644 index 4ca25e62..00000000 Binary files a/assets/close.png and /dev/null differ diff --git a/assets/create.png b/assets/create.png deleted file mode 100644 index b4b0b9a6..00000000 Binary files a/assets/create.png and /dev/null differ diff --git a/assets/create2.png b/assets/create2.png deleted file mode 100644 index 7d60d70e..00000000 Binary files a/assets/create2.png and /dev/null differ diff --git a/assets/delete.png b/assets/delete.png deleted file mode 100644 index 8dc641ea..00000000 Binary files a/assets/delete.png and /dev/null differ diff --git a/assets/dot.png b/assets/dot.png deleted file mode 100644 index bec30918..00000000 Binary files a/assets/dot.png and /dev/null differ diff --git a/assets/file.png b/assets/file.png deleted file mode 100644 index 7109e905..00000000 Binary files a/assets/file.png and /dev/null differ diff --git a/assets/friends.png b/assets/friends.png deleted file mode 100644 index 50e6cc2d..00000000 Binary files a/assets/friends.png and /dev/null differ diff --git a/assets/friends2.png b/assets/friends2.png deleted file mode 100644 index 772333d8..00000000 Binary files a/assets/friends2.png and /dev/null differ diff --git a/assets/hero.png b/assets/hero.png deleted file mode 100644 index 54614568..00000000 Binary files a/assets/hero.png and /dev/null differ diff --git a/assets/img1.gif b/assets/img1.gif deleted file mode 100644 index 18c9c9b8..00000000 Binary files a/assets/img1.gif and /dev/null differ diff --git a/assets/img10.gif b/assets/img10.gif deleted file mode 100644 index f88a3647..00000000 Binary files a/assets/img10.gif and /dev/null differ diff --git a/assets/img2.gif b/assets/img2.gif deleted file mode 100644 index 7e663faa..00000000 Binary files a/assets/img2.gif and /dev/null differ diff --git a/assets/img3.gif b/assets/img3.gif deleted file mode 100644 index 16a85f58..00000000 Binary files a/assets/img3.gif and /dev/null differ diff --git a/assets/img4.gif b/assets/img4.gif deleted file mode 100644 index a3640e0d..00000000 Binary files a/assets/img4.gif and /dev/null differ diff --git a/assets/img5.gif b/assets/img5.gif deleted file mode 100644 index 6294b0df..00000000 Binary files a/assets/img5.gif and /dev/null differ diff --git a/assets/img6.gif b/assets/img6.gif deleted file mode 100644 index 3f1d64fd..00000000 Binary files a/assets/img6.gif and /dev/null differ diff --git a/assets/img7.gif b/assets/img7.gif deleted file mode 100644 index cdd26e42..00000000 Binary files a/assets/img7.gif and /dev/null differ diff --git a/assets/img8.gif b/assets/img8.gif deleted file mode 100644 index 842c24e2..00000000 Binary files a/assets/img8.gif and /dev/null differ diff --git a/assets/img9.gif b/assets/img9.gif deleted file mode 100644 index afae28d6..00000000 Binary files a/assets/img9.gif and /dev/null differ diff --git a/assets/index.js b/assets/index.js deleted file mode 100644 index 6f171ac1..00000000 --- a/assets/index.js +++ /dev/null @@ -1,67 +0,0 @@ -import logo from "./logo.png"; -import close from "./close.png"; -import open from "./open.png"; -import search from "./search.png"; -import avatar from "./avatar.gif"; -import smile from "./smile.png"; -import send from "./send.png"; -import file from "./file.png"; -import user from "./user.png"; -import clear from "./delete.png"; -import create from "./create.png"; -import create2 from "./create2.png"; -import buddy from "./buddy.png"; -import hero from "./hero.png"; -import account from "./account.png"; -import username from "./username.png"; -import friends from "./friends.png"; -import friends2 from "./friends2.png"; -import accountName from "./acountName.png"; -import dot from "./dot.png"; -import loader from "./loader.gif"; -import image1 from "./img1.gif"; -import image2 from "./img2.gif"; -import image3 from "./img3.gif"; -import image4 from "./img4.gif"; -import image5 from "./img5.gif"; -import image6 from "./img6.gif"; -import image7 from "./img7.gif"; -import image8 from "./img8.gif"; -import image9 from "./img9.gif"; -import image10 from "./img10.gif"; - -const images = { - logo, - close, - open, - search, - avatar, - smile, - send, - file, - user, - clear, - create, - create2, - buddy, - hero, - account, - username, - friends, - friends2, - accountName, - dot, - loader, - image1, - image2, - image3, - image4, - image5, - image6, - image7, - image8, - image9, - image10, -}; - -export default images; diff --git a/assets/loader.gif b/assets/loader.gif deleted file mode 100644 index 787940fe..00000000 Binary files a/assets/loader.gif and /dev/null differ diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 5398918f..00000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/assets/open.png b/assets/open.png deleted file mode 100644 index ee7be652..00000000 Binary files a/assets/open.png and /dev/null differ diff --git a/assets/search.png b/assets/search.png deleted file mode 100644 index 1a64c941..00000000 Binary files a/assets/search.png and /dev/null differ diff --git a/assets/send.png b/assets/send.png deleted file mode 100644 index bd677cc6..00000000 Binary files a/assets/send.png and /dev/null differ diff --git a/assets/smile.png b/assets/smile.png deleted file mode 100644 index 6f803a5e..00000000 Binary files a/assets/smile.png and /dev/null differ diff --git a/assets/user.png b/assets/user.png deleted file mode 100644 index 12a63f84..00000000 Binary files a/assets/user.png and /dev/null differ diff --git a/assets/username.png b/assets/username.png deleted file mode 100644 index fa55a670..00000000 Binary files a/assets/username.png and /dev/null differ diff --git a/docs/images/chat.png b/docs/images/chat.png index 582ec972..6e3ef07b 100644 Binary files a/docs/images/chat.png and b/docs/images/chat.png differ diff --git a/docs/images/find-friends.png b/docs/images/find-friends.png index 7109e058..21b91371 100644 Binary files a/docs/images/find-friends.png and b/docs/images/find-friends.png differ diff --git a/docs/images/leaderboard.png b/docs/images/leaderboard.png index bbc76bfb..3ace6784 100644 Binary files a/docs/images/leaderboard.png and b/docs/images/leaderboard.png differ diff --git a/pages/_app.js b/pages/_app.js index ac8b056a..8da6dad9 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,14 +1,16 @@ import Head from "next/head"; +import { Inter } from "next/font/google"; import "../styles/globals.css"; -//INTERNAL IMPORT import { ChatAppProvider } from "../Context/ChatAppContext"; import { NavBar } from "../Components/index"; +const inter = Inter({ subsets: ["latin"] }); + const MyApp = ({ Component, pageProps }) => ( -
+
- Node Connect - Decentralised Chat + Node Connect diff --git a/pages/alluser.js b/pages/alluser.js index 44365b1d..86d5e236 100644 --- a/pages/alluser.js +++ b/pages/alluser.js @@ -1,23 +1,61 @@ import React, { useContext } from "react"; -//INTRNAL IMPORT -import { UserCard } from "../Components/index"; -import Style from "../styles/alluser.module.css"; -import { ChatAppContect } from "../Context/ChatAppContext"; + +import { ChatAppContext } from "../Context/ChatAppContext"; +import { shortAddress } from "../Utils/apiFeature"; +import { Avatar, EmptyState, Spinner } from "../Components/index"; const AllUser = () => { - const { userLists, addFriends } = useContext(ChatAppContect); + const { account, userName, ready, userLists, addFriends, busy } = + useContext(ChatAppContext); + + if (!ready) return
; return ( -
-
-

Any familiar Faces?

+
+
+

Find people

+

Everyone registered on the contract who is not your friend yet.

-
- {userLists.map((el, i) => ( - - ))} -
+ {!account || !userName ? ( + + ) : userLists.length === 0 ? ( + + ) : ( +
    + {userLists.map((u) => { + const adding = busy === `friend:${u.accountAddress}`; + return ( +
  • + +
    +
    {u.name}
    +
    + {shortAddress(u.accountAddress)} +
    +
    + +
  • + ); + })} +
+ )}
); }; diff --git a/pages/index.js b/pages/index.js index 89e02726..d42f7a93 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,17 +1,199 @@ -import React, { useEffect, useState, useContext } from "react"; - -//INTERNAL IMPORT -// import { ChatAppContect } from "../Context/ChatAppContext"; -import { Filter, Friend } from "../Components/index"; - -const ChatApp = () => { - // const {} = useContext(ChatAppContect); - return ( -
- - -
- ); -}; - -export default ChatApp; +import React, { useContext, useEffect, useRef, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +import { ChatAppContext } from "../Context/ChatAppContext"; +import { NETWORK } from "../Context/config"; +import { formatTime, shortAddress } from "../Utils/apiFeature"; +import { Avatar, EmptyState, Spinner } from "../Components/index"; + +const BackIcon = () => ( + +); + +const Conversation = ({ name, address }) => { + const [text, setText] = useState(""); + const end = useRef(null); + const { account, friendMsg, pendingMsg, readMessage, sendMessage, busy } = + useContext(ChatAppContext); + + useEffect(() => { + readMessage(address); + }, [address, readMessage]); + + const mine = pendingMsg.filter((m) => m.to === address); + useEffect(() => { + end.current?.scrollIntoView({ block: "end" }); + }, [friendMsg.length, mine.length]); + + const submit = async (e) => { + e.preventDefault(); + const draft = text; + if (!draft.trim()) return; + setText(""); + if (!(await sendMessage({ msg: draft, address }))) setText(draft); + }; + + return ( +
+
+ + + + +
+
{name}
+
+ {shortAddress(address)} +
+
+
+ +
+ {friendMsg.length === 0 && mine.length === 0 && ( +

+ No messages yet. Say hello; each message you send earns reward + tokens. +

+ )} + {friendMsg.map((m, i) => ( +
+ {m.msg} +
{formatTime(m.timestamp)}
+
+ ))} + {mine.map((m) => ( +
+ {m.msg} +
Waiting for the network
+
+ ))} +
+
+ +
+ + setText(e.target.value)} + /> + +
+
+ ); +}; + +const Chat = () => { + const { account, userName, ready, friendLists, connectWallet, connecting } = + useContext(ChatAppContext); + const { query } = useRouter(); + const address = query.address || ""; + const name = query.name || ""; + + if (!ready) return
; + + if (!account) { + return ( +
+ + + +
+ ); + } + + if (!userName) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + {address ? ( + + ) : ( +
+
+

Pick a friend

+

Choose someone on the left to open the conversation.

+
+
+ )} +
+
+ ); +}; + +export default Chat; diff --git a/pages/leaderboard.js b/pages/leaderboard.js index f837f4b4..b90d302e 100644 --- a/pages/leaderboard.js +++ b/pages/leaderboard.js @@ -1,34 +1,58 @@ -import React, { useEffect, useState, useContext } from "react"; -import { ChatAppContect } from "../Context/ChatAppContext"; -import { connectingWithContract } from "../Utils/apiFeature"; -import { UserCard } from "../Components/index"; -import Style from "../styles/leaderboard.module.css"; +import React, { useContext } from "react"; + +import { ChatAppContext } from "../Context/ChatAppContext"; +import { Avatar, EmptyState } from "../Components/index"; const Leaderboard = () => { -const { userListsforleaderboard } = useContext(ChatAppContect); + const { account, ready, userListsforleaderboard } = useContext(ChatAppContext); + if (!ready) return
; return ( -
-

Leaderboard

- - - - - - - - - - {userListsforleaderboard.map((user, index) => ( - - - - - - ))} - -
RankNameTokens
{index + 1}{user.name}{user.tokens}
+
+
+

Leaderboard

+

Everyone ranked by the reward tokens they have earned.

+
+ + {userListsforleaderboard.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + {userListsforleaderboard.map((u, i) => ( + + + + + + ))} + +
RankName + Tokens +
{i + 1} + + + {u.name} + + {u.tokens}
+
+ )}
); }; diff --git a/styles/alluser.module.css b/styles/alluser.module.css deleted file mode 100644 index 79d94b3e..00000000 --- a/styles/alluser.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.alluser_info { - width: 85%; - margin: 3rem auto; - font-size: 1.6rem; - font-weight: 600; - color: #FFFFFF; - text-align: center; - letter-spacing: 0.5px; -} - -.alluser { - width: 85%; - margin: 2rem auto; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; - padding: 1rem; - background: rgba(41, 47, 63, 0.95); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); -} - -@media only screen and (max-width: 35em) { - .alluser_info { - width: 90%; - font-size: 1.2rem; - margin: 2rem auto; - } - - .alluser { - width: 90%; - grid-template-columns: 1fr; - padding: 0.8rem; - } -} \ No newline at end of file diff --git a/styles/globals.css b/styles/globals.css index 216fdfab..a6670a4a 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,40 +1,623 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); - -html, -body { - padding: 0; - margin: 0; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - background: linear-gradient(180deg, #1E2333 0%, #292F3F 100%); - color: #FFFFFF; - font-size: 16px; -} - -::-webkit-scrollbar { - display: none; -} - -a { - color: inherit; - text-decoration: none; - transition: color 0.3s ease-in-out; -} - -a:hover { - color: #F18303; -} - -* { - box-sizing: border-box; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } - body { - color: #FFFFFF; - background: linear-gradient(180deg, #1E2333 0%, #292F3F 100%); - } -} \ No newline at end of file +/* Design tokens. Every colour, size and radius in the app comes from here. */ +:root { + --bg: #f6f7f9; + --surface: #ffffff; + --surface-2: #f0f2f5; + --border: #dfe3e8; + --text: #14171c; + --muted: #5d6675; + --accent: #0f766e; + --accent-hover: #0b5f58; + --accent-text: #ffffff; + --accent-soft: #e0f2f0; + --danger: #b42318; + --danger-soft: #fdecea; + --mine: #0f766e; + --mine-text: #ffffff; + --theirs: #eceff3; + --radius: 10px; + --radius-sm: 6px; + --shadow: 0 1px 2px rgba(16, 24, 40, 0.06); + --font: system-ui, -apple-system, "Segoe UI", sans-serif; + --mono: ui-monospace, "SFMono-Regular", Consolas, monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0e1013; + --surface: #16191e; + --surface-2: #1d2128; + --border: #2a2f38; + --text: #e8eaee; + --muted: #9099a8; + --accent: #2dd4bf; + --accent-hover: #5eead4; + --accent-text: #06201c; + --accent-soft: #12332f; + --danger: #f97066; + --danger-soft: #3a1a17; + --mine: #14b8a6; + --mine-text: #04201c; + --theirs: #23272f; + --shadow: none; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + color-scheme: light dark; +} + +body { + margin: 0; + font-family: var(--font); + font-size: 15px; + line-height: 1.5; + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; +} + +a { + color: inherit; + text-decoration: none; +} + +h1, +h2, +h3, +p { + margin: 0; +} + +button, +input { + font: inherit; + color: inherit; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +/* Layout */ +.page { + max-width: 1080px; + margin: 0 auto; + padding: 24px 16px 48px; +} + +.page-head { + margin-bottom: 20px; +} + +.page-head h1 { + font-size: 22px; + font-weight: 650; + letter-spacing: -0.01em; +} + +.page-head p { + margin-top: 4px; + color: var(--muted); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 38px; + padding: 0 16px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + font-weight: 550; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.btn:hover:not(:disabled) { + background: var(--surface-2); +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-text); +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); +} + +.btn-small { + min-height: 32px; + padding: 0 12px; + font-size: 14px; +} + +.spinner { + width: 16px; + height: 16px; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .spinner { + animation-duration: 2s; + } +} + +/* Header */ +.header { + position: sticky; + top: 0; + z-index: 10; + background: var(--surface); + border-bottom: 1px solid var(--border); +} + +.header-inner { + max-width: 1080px; + margin: 0 auto; + padding: 0 16px; + min-height: 60px; + display: flex; + align-items: center; + gap: 24px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + letter-spacing: -0.01em; +} + +.brand svg { + color: var(--accent); +} + +.nav { + display: flex; + gap: 4px; + flex: 1; +} + +.nav a { + padding: 6px 12px; + border-radius: var(--radius-sm); + color: var(--muted); + font-weight: 550; +} + +.nav a:hover { + color: var(--text); + background: var(--surface-2); +} + +.nav a[aria-current="page"] { + color: var(--text); + background: var(--surface-2); +} + +.wallet { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); + font-size: 14px; + font-weight: 550; +} + +.chip-balance { + color: var(--accent); + font-variant-numeric: tabular-nums; +} + +/* Avatar */ +.avatar { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: #ffffff; + font-weight: 650; + user-select: none; +} + +/* Panels */ +.panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 48px 24px; + text-align: center; +} + +.empty h2 { + font-size: 17px; + font-weight: 650; +} + +.empty p { + max-width: 380px; + color: var(--muted); +} + +.empty .btn { + margin-top: 8px; +} + +/* Chat */ +.chat { + display: grid; + grid-template-columns: 300px 1fr; + height: calc(100vh - 150px); + min-height: 480px; + overflow: hidden; +} + +.chat-side { + display: flex; + flex-direction: column; + border-right: 1px solid var(--border); + min-width: 0; +} + +.side-title { + padding: 14px 16px; + font-size: 13px; + font-weight: 650; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.friend-list { + flex: 1; + margin: 0; + padding: 0 8px 8px; + list-style: none; + overflow-y: auto; +} + +.friend { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 8px; + border-radius: var(--radius-sm); +} + +.friend:hover { + background: var(--surface-2); +} + +.friend[aria-current="true"] { + background: var(--accent-soft); +} + +.friend-text { + display: flex; + flex-direction: column; + min-width: 0; +} + +.friend-name { + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.friend-address, +.mono { + font-family: var(--mono); + font-size: 12.5px; + color: var(--muted); +} + +.side-empty { + padding: 24px 16px; + color: var(--muted); +} + +.chat-main { + display: flex; + flex-direction: column; + min-width: 0; +} + +.chat-head { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); +} + +.back { + display: none; + padding: 6px; + margin-left: -6px; + border-radius: var(--radius-sm); + color: var(--muted); +} + +.messages { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px; + overflow-y: auto; +} + +.bubble { + max-width: min(520px, 80%); + padding: 8px 12px; + border-radius: 14px; + background: var(--theirs); + overflow-wrap: anywhere; +} + +.bubble.mine { + align-self: flex-end; + background: var(--mine); + color: var(--mine-text); +} + +.bubble-meta { + margin-top: 2px; + font-weight: 400; + font-size: 12px; + opacity: 0.7; + font-variant-numeric: tabular-nums; +} + +.bubble.pending { + opacity: 0.6; +} + +.composer { + display: flex; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--border); +} + +.input { + flex: 1; + min-width: 0; + min-height: 40px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); +} + +.input:focus-visible { + outline-offset: 0; + border-color: var(--accent); +} + +/* People and leaderboard */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; +} + +.person { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; +} + +.person .friend-text { + flex: 1; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.table tbody tr:last-child td { + border-bottom: 0; +} + +.table th { + font-size: 13px; + font-weight: 650; + color: var(--muted); +} + +.table .num { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.table .who { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; +} + +/* Dialog and notice */ +.scrim { + position: fixed; + inset: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + background: rgba(10, 12, 16, 0.55); +} + +.dialog { + width: 100%; + max-width: 420px; + padding: 24px; +} + +.dialog h2 { + font-size: 18px; + font-weight: 650; +} + +.dialog p { + margin-top: 6px; + color: var(--muted); +} + +.field { + margin-top: 16px; +} + +.field label { + display: block; + margin-bottom: 6px; + font-size: 13px; + font-weight: 600; +} + +.field .input { + width: 100%; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 20px; +} + +.notice { + position: fixed; + left: 50%; + bottom: 24px; + z-index: 40; + width: min(480px, calc(100% - 32px)); + transform: translateX(-50%); + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 14px; + border: 1px solid var(--danger); + border-radius: var(--radius); + background: var(--danger-soft); + color: var(--text); +} + +.notice p { + flex: 1; +} + +.notice button { + border: 0; + background: none; + font-weight: 600; + cursor: pointer; + color: var(--danger); +} + +/* Narrow screens: list and chat are separate views */ +@media (max-width: 720px) { + .header-inner { + flex-wrap: wrap; + padding: 8px 16px; + gap: 8px 16px; + } + + .nav { + order: 3; + flex-basis: 100%; + } + + .chat { + grid-template-columns: 1fr; + height: calc(100vh - 190px); + } + + .chat.has-chat .chat-side, + .chat:not(.has-chat) .chat-main { + display: none; + } + + .chat-side { + border-right: 0; + } + + .back { + display: inline-flex; + } + + .chip-name { + display: none; + } +} diff --git a/styles/leaderboard.module.css b/styles/leaderboard.module.css deleted file mode 100644 index 59ad16f0..00000000 --- a/styles/leaderboard.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.LeaderboardContainer { - max-width: 800px; - margin: 2rem auto; - padding: 1rem; - background: linear-gradient( - 145deg, - rgba(41, 47, 63, 0.95), - rgba(20, 25, 40, 0.95) - ); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - color: #fff; -} - -.LeaderboardContainer h1 { - text-align: center; - margin-bottom: 1.5rem; - font-size: 2rem; - color: #f18303; -} - -.LeaderboardTable { - width: 100%; - border-collapse: collapse; -} - -.LeaderboardTable th, -.LeaderboardTable td { - padding: 0.8rem 1rem; - text-align: left; -} - -.LeaderboardTable th { - background: rgba(241, 131, 3, 0.2); - color: #fff; - font-weight: 600; - letter-spacing: 0.5px; -} - -.LeaderboardTable tbody tr { - border-bottom: 1px solid rgba(241, 131, 3, 0.2); - transition: background 0.3s ease-in-out; -} - -.LeaderboardTable tbody tr:nth-child(even) { - background: rgba(255, 255, 255, 0.05); -} - -.LeaderboardTable tbody tr:hover { - background: rgba(241, 131, 3, 0.1); -} - -.LeaderboardTable td { - font-size: 1rem; - color: #fff; -} - -@media only screen and (max-width: 600px) { - .LeaderboardTable th, - .LeaderboardTable td { - padding: 0.6rem 0.8rem; - font-size: 0.9rem; - } - - .LeaderboardContainer h1 { - font-size: 1.5rem; - } -}