Merge pull request #6 from arnelamo/feature/theeme-switch

feat: dark / light mode theme
This commit is contained in:
vilm3r
2023-01-11 18:23:22 -06:00
committed by GitHub
13 changed files with 206 additions and 121 deletions

19
package-lock.json generated
View File

@@ -12,8 +12,9 @@
"@zxing/browser": "^0.1.1",
"date-fns": "^2.11.1",
"gray-matter": "^4.0.2",
"next": "latest",
"next": "12.1.6",
"next-pwa": "^5.5.4",
"next-themes": "^0.2.1",
"nostr-tools": "^1.0.1",
"qrcode.react": "^3.0.2",
"react": "17.0.2",
@@ -6503,6 +6504,16 @@
"next": ">=9.0.0"
}
},
"node_modules/next-themes": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
"integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==",
"peerDependencies": {
"next": "*",
"react": "*",
"react-dom": "*"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
@@ -13924,6 +13935,12 @@
"workbox-window": "^6.5.3"
}
},
"next-themes": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz",
"integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==",
"requires": {}
},
"node-gyp-build": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",

View File

@@ -16,8 +16,9 @@
"@zxing/browser": "^0.1.1",
"date-fns": "^2.11.1",
"gray-matter": "^4.0.2",
"next": "latest",
"next": "12.1.6",
"next-pwa": "^5.5.4",
"next-themes": "^0.2.1",
"nostr-tools": "^1.0.1",
"qrcode.react": "^3.0.2",
"react": "17.0.2",

View File

@@ -8,11 +8,7 @@ type ButtonProps = {
export const Button = ({ children, disabled, onClick, className }: ButtonProps) => {
return (
<div className={className}>
<button
className="bg-custom-black rounded-md px-6 py-3 w-full h-full shadow"
onClick={onClick}
disabled={disabled}
>
<button className="btn-main" onClick={onClick} disabled={disabled}>
{children}
</button>
</div>

View File

@@ -5,10 +5,10 @@ import Link from "next/link"
export function Header() {
return (
<header className="bg-custom-green-light mb-5 mx-auto border-0 rounded-xl p-4 shadow-md">
<div className="container flex justify-between items-center">
<header className="mb-5 p-4">
<div className="container mx-auto flex justify-between items-center">
<a href="/" className="py-1.5 mr-4 text-lg cursor-pointer">
<h1 className="text-xl">Sendstr</h1>
<h1 className="text-2xl text-primary font-semibold">Sendstr</h1>
</a>
<div className="flex items-center space-x-3">
{/* <Link
@@ -17,11 +17,11 @@ export function Header() {
>FAQ
</Link> */}
<a href="https://github.com/vilm3r/sendstr-web">
<DiGithubBadge className="inline text-3xl" title="Github" />
<DiGithubBadge className="inline text-3xl primary-hover" title="Github" />
</a>
<Link href="/settings">
<div className="cursor-pointer">
<MdSettings className="inline text-2xl" title="Settings" />
<MdSettings className="inline text-2xl primary-hover" title="Settings" />
</div>
</Link>
</div>

View File

@@ -15,7 +15,7 @@ export const Input = forwardRef(
return (
<div className={className}>
<input
className="bg-custom-green-dark border-2 border-custom-black rounded w-full p-3"
className="bg-gray-100 dark:bg-gray-800 border-2 border-custom-black rounded w-full p-3"
ref={ref}
value={value}
onChange={onChange}

View File

@@ -5,20 +5,15 @@ type ToggleProps = {
export const Toggle = ({ checked, onChange }: ToggleProps) => {
return (
<label>
<label className="inline-flex relative items-center cursor-pointer">
<input
type="checkbox"
className="absolute overflow-hidden whitespace-nowrap h-[1px] w-[1px]"
className="sr-only peer"
checked={checked}
onChange={onChange}
readOnly
/>
<span className="bg-white border-2 border-custom-black rounded-3xl flex h-8 mr-[10px] relative w-16 cursor-pointer">
<span
className={`flex absolute left-[2px] bottom-[2px] justify-center h-6 w-6 rounded-full items-center transition ${
checked ? "bg-custom-black" : "translate-x-8 bg-custom-black/50"
}`}
></span>
</span>
<div className="w-11 h-6 bg-gray-300 focus:outline-none peer-focus-visible:ring-2 rounded-full dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary"></div>
</label>
)
}

View File

@@ -1,6 +1,7 @@
import "../styles/global.css"
import { AppProps } from "next/app"
import { useEffect, useState } from "react"
import { ThemeProvider } from "next-themes"
export default function App({ Component, pageProps }: AppProps) {
const [keys, setKeys] = useState<{
@@ -17,5 +18,9 @@ export default function App({ Component, pageProps }: AppProps) {
})()
}, [])
return <Component {...{ ...pageProps, keys }} />
return (
<ThemeProvider attribute="class">
<Component {...{ ...pageProps, keys }} />
</ThemeProvider>
)
}

View File

@@ -3,7 +3,6 @@ import Head from "next/head"
import { Header } from "../components/header"
import { Button } from "../components/button"
import { Card } from "../components/card"
import { SendView } from "../views/send"
import { ReceiveView } from "../views/receive"
import { NostrKeysType } from "../types"
@@ -17,28 +16,28 @@ export default function Home({ keys }: HomeProps) {
const LandingView = () => (
<div className="max-w-[64rem] m-auto">
<Card>
<div className="p-10">
<h1 className="text-2xl text-bold pb-5">Open source e2e encrypted bi-directional clipboard</h1>
<p className="pb-10">
Sendstr is an open source end-to-end encrypted bi-directional clipboard app built on top of{" "}
<a className="underline" href="https://github.com/nostr-protocol/nostr" target="_blank">
Nostr
</a>
. No login needed, new throwaway encryption keys are generated on page load, and the
default relay deletes messages after 1 hour. To get started open this page on another
device and choose one of the options below.
</p>
<div className="flex w-full">
<Button className="w-1/2 px-4" onClick={() => setClientType("send")}>
Send
</Button>
<Button className="w-1/2 px-4" onClick={() => setClientType("receive")}>
Receive
</Button>
</div>
<div className="p-10">
<h1 className="font-bold text-5xl md:text-7xl text-bold pb-10">
e2e encrypted shared clipboard
</h1>
<p className="pb-10 leading-relaxed">
Sendstr is an open source end-to-end encrypted shared clipboard app built on top of{" "}
<a className="underline" href="https://github.com/nostr-protocol/nostr" target="_blank">
Nostr
</a>
. No login needed, new throwaway encryption keys are generated on page load, and the
default relay deletes messages after 1 hour. To get started open this page on another
device and choose one of the options below.
</p>
<div className="flex w-full justify-between gap-8">
<Button className="w-1/2 shadow-lg" onClick={() => setClientType("send")}>
Send
</Button>
<Button className="w-1/2 shadow-lg" onClick={() => setClientType("receive")}>
Receive
</Button>
</div>
</Card>
</div>
</div>
)
@@ -57,7 +56,7 @@ export default function Home({ keys }: HomeProps) {
<>
<Head>
<title>Sendstr</title>
<meta name="title" content="Sendstr"/>
<meta name="title" content="Sendstr" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta
name="description"
@@ -85,12 +84,12 @@ export default function Home({ keys }: HomeProps) {
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png"></link>
</Head>
<div className="bg-custom-green-dark min-h-screen">
<div className="min-h-screen">
<div className="p-5">
<div className="max-w-[80rem] mx-auto">
<Header />
</div>
<main>
<main className="max-w-2xl mx-auto px-4">
<Page />
</main>
</div>

View File

@@ -1,6 +1,5 @@
import { createRef, useState } from "react"
import { createRef, useEffect, useState } from "react"
import { Button } from "../../components/button"
import { Card } from "../../components/card"
import { Header } from "../../components/header"
import {
SettingsRelay,
@@ -13,6 +12,7 @@ import { Input } from "../../components/input"
import { MdDelete } from "react-icons/md"
import { Toggle } from "../../components/toggle"
import Head from "next/head"
import { useTheme } from "next-themes"
type SettingsState = {
relays: SettingsRelay[]
@@ -23,12 +23,50 @@ export default function Settings() {
relays: typeof window !== "undefined" ? getRelays() : [],
})
const newPool = createRef<HTMLInputElement>()
const { theme, systemTheme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const ThemeToggle = () => {
if (!mounted) return null
const currentTheme = theme === "system" ? systemTheme : theme
const isDark = currentTheme === "dark"
const toggleHandler = () => setTheme(isDark ? "light" : "dark")
return (
<div onClick={toggleHandler} className="cursor-pointer">
<h2 className="text-2xl pb-5">Theme</h2>
<div className="flex justify-between">
<p className="lg:text-lg">Dark mode</p>
<div className="flex justify-center">
<div>
<div className="inline-flex relative items-center cursor-pointer">
<input
type="checkbox"
value=""
className="sr-only peer"
checked={isDark}
readOnly
/>
<div className="w-11 h-6 bg-gray-300 rounded-full dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary focus:outline-none peer-focus-visible:ring-2"></div>
</div>
</div>
</div>
</div>
</div>
)
}
return (
<>
<Head>
<title>Sendstr - Settings</title>
<meta name="title" content="Sendstr - Settings"/>
<meta name="title" content="Sendstr - Settings" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta
name="description"
@@ -53,13 +91,13 @@ export default function Settings() {
/>
<meta property="twitter:image" content="/favicon-16x16.png" />
</Head>
<div className="bg-custom-green-dark min-h-screen">
<div className="min-h-screen">
<div className="p-5">
<div className="max-w-[80rem] mx-auto">
<Header />
<main className="max-w-[64rem] m-auto">
<Card>
<div className="max-w-[30rem] m-auto p-10">
<div className="max-w-[30rem] m-auto p-10">
<div className="pb-10">
<h2 className="text-2xl pb-5">Relays</h2>
<ul>
{settings.relays.map((relay) => (
@@ -95,7 +133,7 @@ export default function Settings() {
</ul>
<Input className="pt-5" ref={newPool} placeholder="Relay url" />
<Button
className="pt-5"
className="pt-5 shadow-lg"
onClick={() => {
addRelay({
url: newPool?.current?.value || "",
@@ -110,7 +148,10 @@ export default function Settings() {
Add Relay
</Button>
</div>
</Card>
<div className="pt-10">
<ThemeToggle />
</div>
</div>
</main>
</div>
</div>

View File

@@ -2,6 +2,39 @@
@tailwind components;
@tailwind utilities;
body {
color: #FFFFFF
:root {
--orange: #fb8500;
--orange-light: #ffb703;
--green: #004f2d;
--green-light: #7dc95e;
--red: #ff4e33;
--red-light:#ff8370;
}
.light {
--primary: var(--orange);
--secondary: var(--green);
--warning: var(--red);
}
.dark {
--primary: var(--orange-light);
--secondary: var(--green-light);
--warning: var(--red-light);
}
@layer base {
body {
@apply dark:bg-black dark:text-gray-100 bg-white text-gray-900
}
}
@layer components {
.btn-main {
@apply bg-primary rounded-md px-6 py-3 w-full h-full shadow text-gray-100 dark:text-gray-900 font-semibold
}
.primary-hover {
@apply transition duration-300 hover:text-primary
}
}

View File

@@ -1,8 +1,8 @@
import { useTheme } from "next-themes"
import { QRCodeSVG } from "qrcode.react"
import { useEffect, useRef, useState } from "react"
import Toastify from "toastify-js"
import { Button } from "../../components/button"
import { Card } from "../../components/card"
import { getLatestEvent, getReceivePeerKey, sendEncryptedMessage, subscribe } from "../../lib/nostr"
import { debounce } from "../../lib/utils"
import { NostrEventType, NostrKeysType, NostrType } from "../../types"
@@ -36,6 +36,8 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
const [message, setMessage] = useState("")
const events = useRef<{ [k: string]: NostrEventType } | null>(null)
const nostr = useRef<NostrType | null>(null)
const { theme } = useTheme()
const isDarkMode = theme === "dark"
const processEvent = (event: NostrEventType) => {
events.current = { ...events.current, ...{ [event.id]: event } }
@@ -48,7 +50,7 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
nostr.current = { subs, relays, ...keys }
return () => {
nostr?.current?.subs.forEach(sub => sub.unsub())
nostr?.current?.subs.forEach((sub) => sub.unsub())
}
})()
}, [peerKey])
@@ -69,56 +71,54 @@ export const ReceiveView = ({ keys }: ReceiveViewProps) => {
return (
<div className="max-w-[64rem] m-auto">
<Card>
<div className="p-10">
<div className="flex flex-col lg:flex-row">
{peerKey === "" && (
<div className="overflow-visible py-5 max-w-[20rem] mx-auto lg:pr-5">
<QRCodeSVG
value={keys.pub}
level="H"
bgColor="transparent"
fgColor="#3C3744"
includeMargin={false}
width="100%"
height="100%"
/>
<div className="p-10">
<div className="flex flex-col lg:flex-row">
{peerKey === "" && (
<div className="overflow-visible py-5 max-w-[20rem] mx-auto lg:pr-5">
<QRCodeSVG
value={keys.pub}
level="H"
bgColor="transparent"
fgColor={isDarkMode ? "#f3f4f6" : "black"}
includeMargin={false}
width="100%"
height="100%"
/>
</div>
)}
<div className="flex flex-col items-center justify-center w-full">
<div>
<label className="flex flex-grow text-left">My pubkey:</label>
<div
id="mypubkey"
className="border-2 border-custom-black rounded-md p-2 bg-custom-green-dark break-all"
>
{keys.pub}
</div>
)}
<div className="flex flex-col items-center justify-center w-full">
<div>
<label className="flex flex-grow text-left">My pubkey:</label>
<div
id="mypubkey"
className="border-2 border-custom-black rounded-md p-2 bg-custom-green-dark break-all"
<div className="py-6 max-w-[20rem] m-auto">
<Button
className="shadow-lg"
onClick={() => {
navigator.clipboard.writeText(keys.pub).catch(console.warn)
Toastify({
text: "Pubkey copied",
duration: 2000,
close: false,
gravity: "bottom",
position: "center",
stopOnFocus: false,
className: "flex fixed bottom-0 bg-custom-black p-2 rounded left-[45%] z-50",
}).showToast()
}}
>
{keys.pub}
</div>
<div className="py-6 max-w-[20rem] m-auto">
<Button
onClick={() => {
navigator.clipboard.writeText(keys.pub).catch(console.warn)
Toastify({
text: "Pubkey copied",
duration: 2000,
close: false,
gravity: "bottom",
position: "center",
stopOnFocus: false,
className:
"flex fixed bottom-0 bg-custom-black p-2 rounded left-[45%] z-50",
}).showToast()
}}
>
Copy Pubkey
</Button>
</div>
Copy Pubkey
</Button>
</div>
</div>
</div>
<div>{peerKey !== '' && <Message message={message} onChange={onMessageChange} />}</div>
</div>
</Card>
<div>{peerKey !== "" && <Message message={message} onChange={onMessageChange} />}</div>
</div>
</div>
)
}

View File

@@ -5,7 +5,6 @@ import { isMobile } from "react-device-detect"
import { Input } from "../../components/input"
import { MdQrCodeScanner } from "react-icons/md"
import { IconButton } from "../../components/icon-button"
import { Card } from "../../components/card"
import { debounce } from "../../lib/utils"
import { NostrEventType, NostrKeysType, NostrType } from "../../types"
import { subscribe, sendEncryptedMessage, getLatestEvent } from "../../lib/nostr"
@@ -82,7 +81,7 @@ export const SendView = ({ keys }: SendViewProps) => {
const { subs, relays } = await subscribe(keys, peerKey, processEvent)
nostr.current = { subs, relays, ...keys }
return () => {
nostr?.current?.subs.forEach(sub => sub.unsub())
nostr?.current?.subs.forEach((sub) => sub.unsub())
}
})()
}, [])
@@ -117,12 +116,10 @@ export const SendView = ({ keys }: SendViewProps) => {
/>
)}
<div className="mx-auto max-w-[64rem] flex flex-col gap-5">
<Card>
<div className="p-4">
<PeerInput peerKey={peerKey} setShowScan={setShowScan} onChange={setPeerKey} />
{isValidPeerKey(peerKey) && <Message message={message} onChange={onMessageChange} />}
</div>
</Card>
<div className="p-4">
<PeerInput peerKey={peerKey} setShowScan={setShowScan} onChange={setPeerKey} />
{isValidPeerKey(peerKey) && <Message message={message} onChange={onMessageChange} />}
</div>
</div>
</div>
)

View File

@@ -1,18 +1,19 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
"./src/views/**/*.{js,ts,jsx,tsx}",
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
"./src/views/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
theme: {
extend: {
colors: {
'custom-green-light': '#6b9370',
'custom-green-dark': '#4D6A51',
'custom-black': '#3C3744'
}
}
primary: "var(--primary)",
secondary: "var(--secondary)",
warning: "var(--warning)",
},
},
},
plugins: [],
}