feat: patron button and modal

This commit is contained in:
DIYgod 2023-03-14 16:26:25 +00:00
parent 4b1fc327f5
commit ed3e0b2e07
No known key found for this signature in database
12 changed files with 316 additions and 33 deletions

View File

@ -50,5 +50,6 @@
"Showcase": "展示柜",
"AI-generated summary": "AI 生成的摘要",
"Generating": "生成中",
"Show more": "显示更多"
"Show more": "显示更多",
"Patron": "赞助"
}

View File

@ -9,5 +9,10 @@
"powered by": "由 <name/> 提供支持",
"This address is in local editing preview mode and cannot be viewed by the public. The expected online address is:":
"此地址处于本地编辑预览模式,无法被公众查看。预期的在线地址是:",
"Support": "支持"
"View on xChar": "在 xChar 上查看",
"View on xFeed": "在 xFeed 上查看",
"View on Hoot It": "在 Hoot It 上查看",
"View on Crossbell Scan": "在 Crossbell Scan 上查看",
"Subscribe to JSON Feed": "订阅 JSON Feed",
"Subscribe to RSS": "订阅 RSS"
}

View File

@ -102,5 +102,7 @@
"rootless",
"haydenull",
"walk2future",
"cryptonerdcn"
"cryptonerdcn",
"liuyejinghong",
"sion"
]

View File

@ -14,8 +14,17 @@ export const CharacterCard: React.FC<{
open?: boolean
setButtonLoading?: (loading: boolean) => void
hideFollowButton?: boolean
simple?: boolean
style?: "flat" | "normal"
}> = ({ siteId, address, open, setButtonLoading, hideFollowButton, style }) => {
}> = ({
siteId,
address,
open,
setButtonLoading,
hideFollowButton,
simple,
style,
}) => {
const [firstOpen, setFirstOpen] = useState("")
const [site, setSite] = useState<Profile>()
const date = useDate()
@ -41,8 +50,9 @@ export const CharacterCard: React.FC<{
return (
<span
className={cn(
"border-gray-100 rounded-lg space-y-2 text-sm block",
"border-gray-100 rounded-lg text-sm block",
style === "flat" ? "" : "p-4 bg-white shadow-xl",
simple ? "space-y-1" : "space-y-2",
)}
>
{site ? (
@ -69,10 +79,12 @@ export const CharacterCard: React.FC<{
dangerouslySetInnerHTML={{ __html: site?.description || "" }}
></span>
)}
<span className="block">
<FollowingCount siteId={site.username} disableList={true} />
</span>
{site?.date_created && (
{!simple && (
<span className="block">
<FollowingCount siteId={site.username} disableList={true} />
</span>
)}
{!simple && site?.date_created && (
<span className="block text-gray-500">
<time dateTime={date.formatToISO(site.date_created)}>
{t("joined ago", {

View File

@ -65,10 +65,14 @@ export const FollowingButton: React.FC<{
<Button
variant={subscription.data ? "text" : variant}
onClick={handleClickSubscribe}
className={cn(className, "align-middle space-x-1 group", {
"text-accent": subscription.data,
"-mx-2": subscription.data,
})}
className={cn(
className,
"align-middle space-x-1 group border-accent border",
{
"text-accent": subscription.data,
"opacity-60": subscription.data,
},
)}
isLoading={
subscription.data
? unsubscribeFromSite.isLoading || subscribeToSite.isLoading

View File

@ -0,0 +1,34 @@
import { Button } from "~/components/ui/Button"
import { Profile } from "~/lib/types"
import { useTranslation } from "next-i18next"
import { HeartIcon } from "@heroicons/react/24/outline"
import { cn } from "~/lib/utils"
import { Modal } from "~/components/ui/Modal"
import { useState } from "react"
import { PatronModal } from "~/components/common/PatronModal"
export const PatronButton: React.FC<{
site: Profile | undefined | null
className?: string
size?: "sm" | "xl"
loadingStatusChange?: (status: boolean) => void
}> = ({ site, className, size, loadingStatusChange }) => {
const { t } = useTranslation("common")
const [open, setOpen] = useState(false)
return (
<>
<PatronModal open={open} setOpen={setOpen} site={site} />
<Button
variant="text"
aria-label={"Patron"}
key={t("Patron")}
className={cn(className, "-mx-2")}
onClick={() => setOpen(true)}
>
<HeartIcon className="text-red-400 flex w-5 h-5 -mb-[1px]" />
<span className="text-zinc-500 ml-1">Patron</span>
</Button>
</>
)
}

View File

@ -0,0 +1,111 @@
import { Button } from "~/components/ui/Button"
import { Profile } from "~/lib/types"
import { useTranslation } from "next-i18next"
import { HeartIcon } from "@heroicons/react/24/solid"
import { cn } from "~/lib/utils"
import { Modal } from "~/components/ui/Modal"
import { useState } from "react"
import { BoxRadio } from "~/components/ui/BoxRadio"
import { CharacterCard } from "~/components/common/CharacterCard"
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"
import { Tabs } from "../ui/Tabs"
export const PatronModal: React.FC<{
site: Profile | undefined | null
open: boolean
setOpen: (open: boolean) => void
}> = ({ site, open, setOpen }) => {
const { t } = useTranslation("common")
const radios = [
{
text: "🍭 1 Mira",
value: "1",
},
{
text: "☕️ 5 Mira",
value: "5",
},
{
text: "🍕 10 Mira",
value: "10",
},
{
text: "🎁 50 Mira",
value: "50",
},
{
text: "👑 100 Mira",
value: "100",
},
{
text: "Custom",
},
]
const [value, setValue] = useState(radios[1].value!)
const submit = () => {
console.log(value)
}
return (
<Modal
open={open}
setOpen={setOpen}
title={
t("Become a patron to {{name}}", {
name: site?.name,
}) || ""
}
titleIcon={<HeartIcon className="text-red-500 flex w-6 h-6 -mb-[1px]" />}
size="lg"
>
<div className="px-5 py-4 space-y-4">
<CharacterCard
address={site?.metadata?.owner}
open={true}
hideFollowButton={true}
simple={true}
style="flat"
/>
<div className="text-lg">{t("Current patrons")}</div>
<div className="text-zinc-500 text-sm">
{t("You are here to be the first patron.")}
</div>
<div className="text-lg">{t("Select a tier")}</div>
<div>
<Tabs
items={[
{
text: "One-time",
active: true,
},
{
text: "Monthly and NFT Rewards",
tooltip: "Coming soon",
},
]}
></Tabs>
</div>
<div>
<BoxRadio items={radios} value={value} setValue={setValue} />
</div>
<div className="text-zinc-500 text-xs space-y-2">
<p>1 Mira 1 USDC</p>
<p className="flex items-center">
<QuestionMarkCircleIcon className="w-4 h-4 inline-block mr-1" />
What is Mira? Where can I get some?
</p>
</div>
<div>
<Button variant="primary" className="w-full" onClick={submit}>
{t("Become a patron to {{name}}", {
name: site?.name,
})}
</Button>
</div>
</div>
</Modal>
)
}

View File

@ -13,7 +13,7 @@ export const PlanetIcon: React.FC<React.ComponentPropsWithoutRef<"svg">> = (
>
<path
d="M16.4341517,5.34027649 C19.5526401,4.82550436 21.9775728,5.30066885 22.7613144,6.98140814 C23.5450164,8.66206251 22.3504571,10.824965 19.951836,12.8828831 C19.5123956,16.8857302 16.1198862,20 12,20 C10.359675,20 8.83465517,19.5063199 7.56541555,18.6594348 C4.44608523,19.1746732 2.02039254,18.6996452 1.23650448,17.0185919 C0.452540248,15.3373751 1.64816139,13.1735876 4.04839101,11.1150512 C4.48877158,7.11319839 7.88082257,4 12,4 C13.6401386,4 15.1649982,4.49356789 16.4341517,5.34027649 Z M18.3526801,7.13687265 C19.0478987,8.04366602 19.553342,9.10361155 19.8096504,10.2573495 C20.759634,9.23569807 21.1931253,8.35081879 20.9486988,7.82664467 C20.7041658,7.30224176 19.7469377,7.06554811 18.3526801,7.13687265 Z M5.64723739,16.8630198 C4.95168113,15.9557542 4.44609504,14.8951833 4.1899293,13.7407574 C3.23872065,14.7632088 2.80454268,15.6488575 3.04912005,16.1733553 C3.29377581,16.6980213 4.25184178,16.9346893 5.64723739,16.8630198 Z M17.998801,11.8788388 C17.9342721,8.62105818 15.2732265,6 12,6 C8.6862915,6 6,8.6862915 6,12 C6,13.8017545 6.79417414,15.4180196 8.05156374,16.5178367 C9.61918795,16.1740501 11.4219878,15.5791568 13.2667642,14.7189234 C15.1123744,13.8583012 16.727532,12.8591526 17.998801,11.8788388 Z M17.3040733,14.8073455 C16.3274273,15.4175193 15.2552552,15.9984306 14.1120008,16.5315389 C12.9695057,17.0642931 11.8360704,17.5119809 10.7415499,17.8678087 C11.1473536,17.9544194 11.5683538,18 12,18 C14.2994717,18 16.2968203,16.7064576 17.3040733,14.8073455 Z"
fill-rule="evenodd"
fillRule="evenodd"
/>
</svg>
)

View File

@ -19,6 +19,8 @@ import { useState, useRef, useEffect, RefObject } from "react"
import chroma from "chroma-js"
import { Menu } from "~/components/ui/Menu"
import { useTranslation } from "next-i18next"
import { Tooltip } from "~/components/ui/Tooltip"
import { PatronButton } from "~/components/common/PatronButton"
type HeaderLinkType = {
icon?: React.ReactNode
@ -92,7 +94,7 @@ export const SiteHeader: React.FC<{
url: `${CSB_SCAN}/address/${site?.metadata?.owner}`,
},
{
text: "JSON Feed",
text: "Subscribe to JSON Feed",
icon: (
<div className="w-full h-full">
<Image
@ -107,7 +109,7 @@ export const SiteHeader: React.FC<{
url: `/feed`,
},
{
text: "RSS",
text: "Subscribe to RSS",
icon: <RssIcon className="w-full h-full text-[#ee832f]" />,
url: `/feed/xml`,
out: true,
@ -207,12 +209,12 @@ export const SiteHeader: React.FC<{
imageRef={avatarRef}
/>
)}
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 relative">
<div className="flex items-center justify-between">
<div className="xlog-site-name text-2xl sm:text-3xl font-bold text-zinc-900 leading-snug break-words min-w-0">
{site?.name}
</div>
<div className="ml-4 sm:ml-8 space-x-3 sm:space-x-6 flex items-center">
<div className="ml-4 sm:ml-8 space-x-3 sm:space-x-4 flex items-center sm:static absolute -bottom-0 right-0">
<div className="xlog-site-more-menu relative inline-block align-middle">
<Menu
target={
@ -236,7 +238,7 @@ export const SiteHeader: React.FC<{
<span className="fill-gray-500 flex w-4 h-4">
{item.icon}
</span>
<span>{item.text}</span>
<span>{t(item.text)}</span>
</UniLink>
)
})}
@ -248,17 +250,22 @@ export const SiteHeader: React.FC<{
{moreMenuItems.map((item) => {
if (item.out) {
return (
<Button
variant="text"
aria-label={item.text}
<Tooltip
label={t(item.text)}
key={item.text}
className="-mx-2"
onClick={() => window.open(item.url, "_blank")}
placement="bottom"
>
<span className="fill-gray-500 flex w-6 h-6">
{item.icon}
</span>
</Button>
<Button
variant="text"
aria-label={item.text}
className="-mx-2"
onClick={() => window.open(item.url, "_blank")}
>
<span className="fill-gray-500 flex w-6 h-6">
{item.icon}
</span>
</Button>
</Tooltip>
)
} else {
return null
@ -276,8 +283,13 @@ export const SiteHeader: React.FC<{
dangerouslySetInnerHTML={{ __html: site?.description || "" }}
></div>
)}
<div className="xlog-site-follow-count">
<FollowingCount siteId={site?.username} />
<div className="flex space-x-0 sm:space-x-5 space-y-2 sm:space-y-0 flex-col sm:flex-row">
<span className="xlog-site-follow-count block sm:inline-block">
<FollowingCount siteId={site?.username} />
</span>
<span className="xlog-site-patron">
<PatronButton site={site} />
</span>
</div>
</div>
</div>

View File

@ -0,0 +1,92 @@
import { cn } from "~/lib/utils"
import React, {
ChangeEvent,
FocusEvent,
Dispatch,
SetStateAction,
useMemo,
useState,
} from "react"
import { UniLink } from "./UniLink"
import { useTranslation } from "next-i18next"
import { nanoid } from "nanoid"
import { Input } from "~/components/ui/Input"
export type RadioItem = {
text: string
value?: string
default?: boolean
}
export const BoxRadio: React.FC<{
value: string
setValue: Dispatch<SetStateAction<string>>
items: RadioItem[]
}> = ({ value, setValue, items }) => {
const { t } = useTranslation(["dashboard"])
const randomId = useMemo(() => nanoid(), [])
const [isCustom, setIsCustom] = useState(false)
const toCustom = (
e: ChangeEvent<HTMLInputElement> | FocusEvent<HTMLInputElement>,
) => {
setIsCustom(true)
setValue(e.target.value)
}
const toDefined = (e: ChangeEvent<HTMLInputElement>) => {
setIsCustom(false)
setValue(e.target.value)
}
return (
<div className="grid gap-4 grid-cols-3">
{items.map((item) => {
return (
<div key={item.value} className="relative w-full min-h-[80px]">
{item.value ? (
<>
<input
className={cn(
"opacity-0 absolute left-0 right-0 top-0 bottom-0 pointer-events-none",
)}
type="radio"
id={`${randomId}-${item.value}`}
name={randomId}
value={item.value}
checked={value === item.value}
onChange={toDefined}
/>
<label
className={cn(
"absolute left-0 right-0 top-0 bottom-0 flex items-center justify-center rounded-lg cursor-pointer border",
value === item.value
? "border-accent border-2 text-accent"
: "",
)}
htmlFor={`${randomId}-${item.value}`}
>
{t(item.text)}
</label>
</>
) : (
<>
<Input
className={cn("w-full min-h-[80px] text-center", {
"border-accent border-2 text-accent": isCustom,
})}
type="text"
id={`${randomId}-${item.value}`}
name={randomId}
placeholder={t(item.text) || ""}
onChange={toCustom}
onFocus={toCustom}
/>
</>
)}
</div>
)
})}
</div>
)
}

View File

@ -2,6 +2,7 @@ import { cn } from "~/lib/utils"
import React from "react"
import { UniLink } from "./UniLink"
import { useTranslation } from "next-i18next"
import { Tooltip } from "~/components/ui/Tooltip"
export type TabItem = {
text: string
@ -9,6 +10,7 @@ export type TabItem = {
hidden?: boolean
onClick?: () => void
active?: boolean
tooltip?: string
}
export const Tabs: React.FC<{ items: TabItem[] }> = ({ items }) => {
@ -25,13 +27,19 @@ export const Tabs: React.FC<{ items: TabItem[] }> = ({ items }) => {
onClick={item.onClick}
key={item.text}
className={cn(
`border-b-2 inline-flex items-center h-10 whitespace-nowrap`,
`border-b-2 inline-flex items-center h-10 whitespace-nowrap cursor-pointer`,
item.active
? `border-accent text-black font-medium`
: `text-gray-500 border-transparent hover:border-gray-300`,
)}
>
{t(item.text)}
{item.tooltip ? (
<Tooltip label={item.tooltip}>
<>{t(item.text)}</>
</Tooltip>
) : (
<>{t(item.text)}</>
)}
</UniLink>
)
})}

View File

@ -20,6 +20,7 @@ interface Props {
placement?: Placement
children: JSX.Element
className?: string
inline?: boolean
}
export const Tooltip = ({
@ -27,6 +28,7 @@ export const Tooltip = ({
label,
placement = "top",
className,
inline,
}: Props) => {
const [open, setOpen] = useState(false)
@ -54,7 +56,7 @@ export const Tooltip = ({
<div
ref={refs.setReference}
{...getReferenceProps()}
className="flex items-center"
className={cn("items-center", inline ? "inline-flex" : "flex")}
>
{children}
</div>