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", "@zxing/browser": "^0.1.1",
"date-fns": "^2.11.1", "date-fns": "^2.11.1",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
"next": "latest", "next": "12.1.6",
"next-pwa": "^5.5.4", "next-pwa": "^5.5.4",
"next-themes": "^0.2.1",
"nostr-tools": "^1.0.1", "nostr-tools": "^1.0.1",
"qrcode.react": "^3.0.2", "qrcode.react": "^3.0.2",
"react": "17.0.2", "react": "17.0.2",
@@ -6503,6 +6504,16 @@
"next": ">=9.0.0" "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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.5", "version": "8.4.5",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
@@ -13924,6 +13935,12 @@
"workbox-window": "^6.5.3" "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": { "node-gyp-build": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", "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", "@zxing/browser": "^0.1.1",
"date-fns": "^2.11.1", "date-fns": "^2.11.1",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
"next": "latest", "next": "12.1.6",
"next-pwa": "^5.5.4", "next-pwa": "^5.5.4",
"next-themes": "^0.2.1",
"nostr-tools": "^1.0.1", "nostr-tools": "^1.0.1",
"qrcode.react": "^3.0.2", "qrcode.react": "^3.0.2",
"react": "17.0.2", "react": "17.0.2",

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ export const Input = forwardRef(
return ( return (
<div className={className}> <div className={className}>
<input <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} ref={ref}
value={value} value={value}
onChange={onChange} onChange={onChange}

View File

@@ -5,20 +5,15 @@ type ToggleProps = {
export const Toggle = ({ checked, onChange }: ToggleProps) => { export const Toggle = ({ checked, onChange }: ToggleProps) => {
return ( return (
<label> <label className="inline-flex relative items-center cursor-pointer">
<input <input
type="checkbox" type="checkbox"
className="absolute overflow-hidden whitespace-nowrap h-[1px] w-[1px]" className="sr-only peer"
checked={checked} checked={checked}
onChange={onChange} onChange={onChange}
readOnly
/> />
<span className="bg-white border-2 border-custom-black rounded-3xl flex h-8 mr-[10px] relative w-16 cursor-pointer"> <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>
<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>
</label> </label>
) )
} }

View File

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

View File

@@ -1,6 +1,5 @@
import { createRef, useState } from "react" import { createRef, useEffect, useState } from "react"
import { Button } from "../../components/button" import { Button } from "../../components/button"
import { Card } from "../../components/card"
import { Header } from "../../components/header" import { Header } from "../../components/header"
import { import {
SettingsRelay, SettingsRelay,
@@ -13,6 +12,7 @@ import { Input } from "../../components/input"
import { MdDelete } from "react-icons/md" import { MdDelete } from "react-icons/md"
import { Toggle } from "../../components/toggle" import { Toggle } from "../../components/toggle"
import Head from "next/head" import Head from "next/head"
import { useTheme } from "next-themes"
type SettingsState = { type SettingsState = {
relays: SettingsRelay[] relays: SettingsRelay[]
@@ -23,12 +23,50 @@ export default function Settings() {
relays: typeof window !== "undefined" ? getRelays() : [], relays: typeof window !== "undefined" ? getRelays() : [],
}) })
const newPool = createRef<HTMLInputElement>() 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 ( return (
<> <>
<Head> <Head>
<title>Sendstr - Settings</title> <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="viewport" content="initial-scale=1.0, width=device-width" />
<meta <meta
name="description" name="description"
@@ -53,13 +91,13 @@ export default function Settings() {
/> />
<meta property="twitter:image" content="/favicon-16x16.png" /> <meta property="twitter:image" content="/favicon-16x16.png" />
</Head> </Head>
<div className="bg-custom-green-dark min-h-screen"> <div className="min-h-screen">
<div className="p-5"> <div className="p-5">
<div className="max-w-[80rem] mx-auto"> <div className="max-w-[80rem] mx-auto">
<Header /> <Header />
<main className="max-w-[64rem] m-auto"> <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> <h2 className="text-2xl pb-5">Relays</h2>
<ul> <ul>
{settings.relays.map((relay) => ( {settings.relays.map((relay) => (
@@ -95,7 +133,7 @@ export default function Settings() {
</ul> </ul>
<Input className="pt-5" ref={newPool} placeholder="Relay url" /> <Input className="pt-5" ref={newPool} placeholder="Relay url" />
<Button <Button
className="pt-5" className="pt-5 shadow-lg"
onClick={() => { onClick={() => {
addRelay({ addRelay({
url: newPool?.current?.value || "", url: newPool?.current?.value || "",
@@ -110,7 +148,10 @@ export default function Settings() {
Add Relay Add Relay
</Button> </Button>
</div> </div>
</Card> <div className="pt-10">
<ThemeToggle />
</div>
</div>
</main> </main>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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