activity tab and contact viewer

This commit is contained in:
Paul Miller
2023-05-03 16:49:44 -05:00
parent 20b2c0bd8f
commit a379f417e8
13 changed files with 293 additions and 182 deletions

View File

@@ -16,7 +16,7 @@ type RadioGroupProps = {
};
type Color = "blue" | "green" | "red" | "gray"
const colorVariants = {
export const colorVariants = {
blue: "bg-m-blue",
green: "bg-m-green",
red: "bg-m-red",

View File

@@ -1,56 +1,47 @@
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
import { Button, LargeHeader, VStack } from '~/components/layout';
import { useMegaStore } from '~/state/megaStore';
import { satsToUsd } from '~/utils/conversions';
import { Amount } from './Amount';
import { Dialog, RadioGroup } from '@kobalte/core';
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
import { SmallHeader } from '~/components/layout';
import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg";
import { SubmitHandler, createForm, required, reset, setValue } from '@modular-forms/solid';
import { TextField } from './layout/TextField';
import { ColorRadioGroup } from './ColorRadioGroup';
import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm';
type Color = "blue" | "green" | "red" | "gray"
const INITIAL: ContactItem = { id: createUniqueId(), kind: "contact", name: "", color: "gray" }
type Contact = {
name: string;
npub?: string;
isExchange: boolean;
color: string;
}
// const colorOptions = ["blue", "green", "red", "gray"]
const colorOptions = [{ label: "blue", value: "blue" }, { label: "green", value: "green" }, { label: "red", value: "red" }, { label: "gray", value: "gray" }]
const INITIAL: Contact = { name: "", isExchange: false, color: "gray" }
export function ContactEditor(props: { createContact: (name: string) => void }) {
const [isOpen, setIsOpen] = createSignal(true);
const [contactForm, { Form, Field }] = createForm<Contact>({ initialValues: INITIAL });
export function ContactEditor(props: { createContact: (contact: ContactItem) => void, list?: boolean }) {
const [isOpen, setIsOpen] = createSignal(false);
// What we're all here for in the first place: returning a value
const handleSubmit: SubmitHandler<Contact> = (c: Contact) => {
// e.preventDefault()
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
// TODO: why do the id and color disappear?
props.createContact(c.name)
const odd = { id: createUniqueId(), kind: "contact" }
props.createContact({ ...odd, ...c })
setIsOpen(false);
}
createEffect(() => {
// When isOpen changes we reset the form
if (isOpen()) {
reset(contactForm, { initialValues: INITIAL })
}
})
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return (
<Dialog.Root isOpen={isOpen()}>
<button onClick={() => setIsOpen(true)} class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button>
<Switch>
<Match when={props.list}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2">
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
<span class="leading-[4rem]">+</span>
</div>
<SmallHeader class="overflow-ellipsis">
new
</SmallHeader>
</button>
</Match>
<Match when={!props.list}>
<button onClick={() => setIsOpen(true)} class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button>
</Match>
</Switch>
<Dialog.Portal>
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
@@ -59,32 +50,7 @@ export function ContactEditor(props: { createContact: (name: string) => void })
<img src={close} alt="Close" />
</button>
</div>
<Form onSubmit={handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
<div>
<LargeHeader>Create Contact</LargeHeader>
<VStack>
<Field name="name" validate={[required("We at least need a name")]}>
{(field, props) => (
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
)}
</Field>
<Field name="npub" validate={[]}>
{(field, props) => (
<TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" />
)}
</Field>
<Field name="color">
{(field, props) => (
<ColorRadioGroup options={colorOptions} {...props} value={field.value} error={field.error} label="Color" />
)}
</Field>
</VStack>
</div>
<Button type="submit" intent="blue" class="w-full flex-none">
Create Contact
</Button>
</Form>
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} initialValues={INITIAL} />
</Dialog.Content>
</div>
</Dialog.Portal>

View File

@@ -0,0 +1,39 @@
import { SubmitHandler, createForm, required } from "@modular-forms/solid";
import { ContactItem } from "~/state/contacts";
import { Button, LargeHeader, VStack } from "~/components/layout";
import { TextField } from "~/components/layout/TextField";
import { ColorRadioGroup } from "~/components/ColorRadioGroup";
const colorOptions = [{ label: "blue", value: "blue" }, { label: "green", value: "green" }, { label: "red", value: "red" }, { label: "gray", value: "gray" }]
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, initialValues?: ContactItem, title: string, cta: string }) {
const [_contactForm, { Form, Field }] = createForm<ContactItem>({ initialValues: props.initialValues });
return (
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
<div>
<LargeHeader>{props.title}</LargeHeader>
<VStack>
<Field name="name" validate={[required("We at least need a name")]}>
{(field, props) => (
<TextField {...props} placeholder='Satoshi' value={field.value} error={field.error} label="Name" />
)}
</Field>
<Field name="npub" validate={[]}>
{(field, props) => (
<TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" />
)}
</Field>
<Field name="color">
{(field, props) => (
<ColorRadioGroup options={colorOptions} {...props} value={field.value} error={field.error} label="Color" />
)}
</Field>
</VStack>
</div>
<Button type="submit" intent="blue" class="w-full flex-none">
{props.cta}
</Button>
</Form>
)
}

View File

@@ -0,0 +1,71 @@
import { Match, Switch, createSignal } from 'solid-js';
import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
import { Dialog } from '@kobalte/core';
import close from "~/assets/icons/close.svg";
import { SubmitHandler } from '@modular-forms/solid';
import { ContactItem } from '~/state/contacts';
import { ContactForm } from './ContactForm';
export function ContactViewer(props: { contact: ContactItem, gradient: string, saveContact: (contact: ContactItem) => void }) {
const [isOpen, setIsOpen] = createSignal(false);
const [isEditing, setIsEditing] = createSignal(false);
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
props.saveContact({ ...props.contact, ...c })
setIsEditing(false)
}
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-xl bg-neutral-800/70"
return (
<Dialog.Root isOpen={isOpen()}>
<button onClick={() => setIsOpen(true)} class="flex flex-col items-center gap-2 w-16 flex-shrink-0 overflow-x-hidden">
<div class="flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: props.gradient }}
>
{props.contact.name[0]}
</div>
<SmallHeader class="overflow-ellipsis w-16 text-center overflow-hidden h-4">
{props.contact.name}
</SmallHeader>
</button>
<Dialog.Portal>
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT} onEscapeKeyDown={() => setIsOpen(false)}>
<div class="w-full flex justify-end">
<button tabindex="-1" onClick={() => setIsOpen(false)} class="hover:bg-white/10 rounded-lg active:bg-m-blue">
<img src={close} alt="Close" />
</button>
</div>
<Switch>
<Match when={isEditing()}>
<ContactForm title="Edit contact" cta="Save contact" handleSubmit={handleSubmit} initialValues={props.contact} />
</Match>
<Match when={!isEditing()}>
<div class="flex flex-col flex-1 justify-around items-center gap-4 max-w-[400px] mx-auto w-full">
<div class="flex flex-col items-center w-full">
<div class="flex-none h-32 w-32 rounded-full flex items-center justify-center text-8xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: props.gradient }}
>
{props.contact.name[0]}
</div>
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">{props.contact.name}</h1>
<Card title="Payment history">
<NiceP>No payments yet with <span class="font-semibold">{props.contact.name}</span></NiceP>
</Card>
</div>
<div class="flex w-full gap-2">
<Button layout="flex" intent="green" onClick={() => setIsEditing(true)}>Edit</Button>
<Button intent="blue" onClick={() => { }}>Pay</Button>
</div>
</div>
</Match>
</Switch>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root >
);
}

View File

@@ -2,10 +2,11 @@ import mutiny_m from '~/assets/icons/m.svg';
import airplane from '~/assets/icons/airplane.svg';
import settings from '~/assets/icons/settings.svg';
import receive from '~/assets/icons/big-receive.svg';
import userClock from '~/assets/icons/user-clock.svg';
import { A } from "solid-start";
type ActiveTab = 'home' | 'scan' | 'send' | 'receive' | 'settings' | 'none';
type ActiveTab = 'home' | 'scan' | 'send' | 'receive' | 'settings' | 'activity' | 'none';
export default function NavBar(props: { activeTab: ActiveTab }) {
const activeStyle = 'border-t-0 border-b-0 p-2 bg-black rounded-lg'
@@ -28,6 +29,11 @@ export default function NavBar(props: { activeTab: ActiveTab }) {
<img src={receive} alt="receive" />
</A>
</li>
<li class={props.activeTab === "activity" ? activeStyle : inactiveStyle}>
<A href="/activity">
<img src={userClock} alt="activity" />
</A>
</li>
<li class={props.activeTab === "settings" ? activeStyle : inactiveStyle}>
<A href="/settings">
<img src={settings} alt="settings" />

View File

@@ -1,8 +1,9 @@
import { Select, createOptions } from "@thisbeyond/solid-select";
import "~/styles/solid-select.css"
import { SmallHeader } from "./layout";
import { For } from "solid-js";
import { For, createUniqueId } from "solid-js";
import { ContactEditor } from "./ContactEditor";
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
// take two arrays, subtract the second from the first, then return the first
function subtract<T>(a: T[], b: T[]) {
@@ -10,20 +11,12 @@ function subtract<T>(a: T[], b: T[]) {
return a.filter(x => !set.has(x));
};
// simple math.random based id generator
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
export type TagItem = {
id: string;
name: string;
kind: "text" | "contact";
}
const createValue = (name: string) => {
const createValue = (name: string): TextItem => {
return { id: createUniqueId(), name, kind: "text" };
};
export function TagEditor(props: { title: string, values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void }) {
console.log(props.values);
const onChange = (selected: TagItem[]) => {
props.setSelectedValues(selected);
@@ -42,8 +35,8 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
createable: createValue,
});
const newContact = (name: string) => {
const contact: TagItem = { id: createUniqueId(), name, kind: "contact" };
const newContact = async (contact: ContactItem) => {
await addContact(contact)
onChange([...props.selectedValues, contact])
}
@@ -59,8 +52,13 @@ export function TagEditor(props: { title: string, values: TagItem[], setValues:
/>
<div class="flex gap-2 flex-wrap">
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
{(contact) => (
<div onClick={() => onChange([...props.selectedValues, contact])} class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-m-blue px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">{contact.name}</div>
{(tag) => (
<div onClick={() => onChange([...props.selectedValues, tag])}
class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1"
classList={{ "bg-black": tag.kind === "text", "bg-m-blue": tag.kind === "contact" && tag.color === "blue", "bg-m-green": tag.kind === "contact" && tag.color === "green", "bg-m-red": tag.kind === "contact" && tag.color === "red", "bg-[#898989]": tag.kind === "contact" && tag.color === "gray" }}
>
{tag.name}
</div>
)}
</For>
{/* <button class="border border-l-white/50 border-r-white/50 border-t-white/75 border-b-white/25 bg-black px-1 py-[0.5] rounded cursor-pointer hover:outline-white hover:outline-1">+ Add Contact</button> */}

View File

@@ -16,7 +16,7 @@ export const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
export const Card: ParentComponent<{ title?: string, titleElement?: JSX.Element }> = (props) => {
return (
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden w-full'>
{props.title && <SmallHeader>{props.title}</SmallHeader>}
{props.titleElement && props.titleElement}
{props.children}
@@ -94,8 +94,15 @@ export const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
export const Hr = () => <Separator.Root class="my-4 border-white/20" />
export const LargeHeader: ParentComponent = (props) => {
return (<h1 class="text-4xl font-semibold uppercase border-b-2 border-b-white mt-2 mb-4">{props.children}</h1>)
export const LargeHeader: ParentComponent<{ action?: JSX.Element }> = (props) => {
return (
<header class="w-full flex justify-between items-center mt-4 mb-2">
<h1 class="text-4xl font-semibold">{props.children}</h1>
<Show when={props.action}>
{props.action}
</Show>
</header>
)
}
export const VStack: ParentComponent<{ biggap?: boolean }> = (props) => {
@@ -106,8 +113,8 @@ export const HStack: ParentComponent<{ biggap?: boolean }> = (props) => {
return (<div class={`flex gap-${props.biggap ? "8" : "4"}`}>{props.children}</div>)
}
export const SmallAmount: ParentComponent<{ amount: number | bigint }> = (props) => {
return (<h2 class="font-light text-lg">{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
export const SmallAmount: ParentComponent<{ amount: number | bigint, sign?: string }> = (props) => {
return (<h2 class="font-light text-lg">{props.sign ? `${props.sign} ` : ""}{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
}
export const NiceP: ParentComponent = (props) => {