feat: patron button and modal
This commit is contained in:
parent
4b1fc327f5
commit
ed3e0b2e07
|
|
@ -50,5 +50,6 @@
|
|||
"Showcase": "展示柜",
|
||||
"AI-generated summary": "AI 生成的摘要",
|
||||
"Generating": "生成中",
|
||||
"Show more": "显示更多"
|
||||
"Show more": "显示更多",
|
||||
"Patron": "赞助"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -102,5 +102,7 @@
|
|||
"rootless",
|
||||
"haydenull",
|
||||
"walk2future",
|
||||
"cryptonerdcn"
|
||||
"cryptonerdcn",
|
||||
"liuyejinghong",
|
||||
"sion"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue