feat: can claim daily

This commit is contained in:
0xzio
2024-11-15 15:23:22 +08:00
parent bcf5aaee87
commit 46c3602e42
19 changed files with 533 additions and 67 deletions

View File

@@ -1,3 +1,4 @@
import { Airdrop } from '@/components/Airdrop/Airdrop'
import { ModeToggle } from '@/components/ModeToggle'
import { Profile } from '@/components/Profile/Profile'
import { getSite } from '@/lib/fetchers'
@@ -11,7 +12,6 @@ export default async function RootLayout({
}: {
children: React.ReactNode
}) {
const site = await getSite()
const { SiteLayout } = await loadTheme()
@@ -22,6 +22,7 @@ export default async function RootLayout({
ModeToggle={ModeToggle}
MobileNav={null}
ConnectButton={Profile}
Airdrop={Airdrop}
>
{children}
</SiteLayout>

View File

@@ -74,7 +74,10 @@ export const BuyBtn = ({
if (!address)
return (
<WalletConnectButton className="h-[50px] w-full rounded-xl">
<WalletConnectButton
variant="brand"
className="h-[50px] w-full rounded-xl"
>
Connect wallet
</WalletConnectButton>
)

View File

@@ -97,7 +97,10 @@ export const SellBtn = ({
)}
</Button>
) : (
<WalletConnectButton className="h-[50px] w-full rounded-full">
<WalletConnectButton
variant="brand"
className="h-[50px] w-full rounded-full"
>
Connect wallet
</WalletConnectButton>
)}

View File

@@ -0,0 +1,12 @@
import * as React from 'react'
import { AirdropButton } from './AirdropButton'
import { AirdropDialog } from './AirdropDialog'
export function Airdrop() {
return (
<>
{/* <AirdropButton /> */}
<AirdropDialog />
</>
)
}

View File

@@ -0,0 +1,13 @@
'use client'
import * as React from 'react'
import { Rocket } from 'lucide-react'
export function AirdropButton() {
return (
<div className="flex items-center gap-1 text-xs rounded-full px-2 py-1 text-foreground/50 font-medium cursor-pointer hover:text-foreground/80 h-8 transition-colors">
<Rocket size={12} />
<div>Airdrop</div>
</div>
)
}

View File

@@ -0,0 +1,163 @@
'use client'
import { useState } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { useDailyClaimCap } from '@/hooks/useDailyClaimCap'
import { useWagmiConfig } from '@/hooks/useWagmiConfig'
import { dailyClaimAbi } from '@/lib/abi'
import { addressMap } from '@/lib/address'
import { extractErrorMessage } from '@/lib/extractErrorMessage'
import { readContract, waitForTransactionReceipt } from '@wagmi/core'
import { useSession } from 'next-auth/react'
import pRetry, { AbortError } from 'p-retry'
import { toast } from 'sonner'
import { useAccount, useReadContract, useWriteContract } from 'wagmi'
import LoadingDots from '../icons/loading-dots'
import { Button } from '../ui/button'
import { Skeleton } from '../ui/skeleton'
import { WalletConnectButton } from '../WalletConnectButton'
import { AirdropButton } from './AirdropButton'
interface Props {}
export function AirdropDialog({}: Props) {
const { data } = useSession()
return (
<>
<Dialog>
<DialogTrigger>
<AirdropButton />
</DialogTrigger>
<DialogContent className="sm:max-w-[400px] min-h-96 flex flex-col gap-6">
<DialogHeader className="text-center">
<DialogTitle className="text-center">
Claim $PEN everyday!
</DialogTitle>
</DialogHeader>
{data && <AuthenticatedContent />}
{!data && <UnauthenticatedContent />}
</DialogContent>
</Dialog>
</>
)
}
function UnauthenticatedContent() {
return (
<>
<div className="w-11/12 text-foreground/60 text-center mx-auto">
ou can claim some $PEN every day. Connect your wallet to see how much
$PEN youre eligible to claim.
</div>
<div className="flex items-center justify-center gap-2">
<div className="text-3xl font-bold text-center">*** $PEN</div>
<div className="text-foreground/50">claimable</div>
</div>
<div className="flex items-center justify-center flex-1 -mt-10">
<WalletConnectButton size="lg">
Connect wallet to Claim $PEN
</WalletConnectButton>
</div>
</>
)
}
function AuthenticatedContent() {
const [isClaiming, setIsClaiming] = useState(false)
const { data, isLoading } = useDailyClaimCap()
const { writeContractAsync } = useWriteContract()
const { address } = useAccount()
const wagmiConfig = useWagmiConfig()
let { data: executionFee, error } = useReadContract({
address: addressMap.DailyClaim,
abi: dailyClaimAbi,
functionName: 'executionFee',
})
async function checkClaimSuccess() {
const requests = await readContract(wagmiConfig, {
address: addressMap.DailyClaim,
abi: dailyClaimAbi,
functionName: 'getPendingRequests',
})
const userRequests = requests.filter((req) => req.user === address) || []
// Abort retrying if the resource doesn't exist
if (userRequests.length > 0) {
// throw new AbortError('Not success')
throw new Error('Not success')
}
return true
}
async function claim() {
setIsClaiming(true)
try {
const hash = await writeContractAsync({
abi: dailyClaimAbi,
address: addressMap.DailyClaim,
functionName: 'createClaimRequest',
value: executionFee,
})
await waitForTransactionReceipt(wagmiConfig, { hash })
await pRetry(checkClaimSuccess, {
retries: 20,
minTimeout: 1000,
onFailedAttempt(error) {
console.log('=====error:', error.attemptNumber, error.name)
},
})
toast.success('Claim $PEN successfully')
} catch (error) {
console.log('======error:', error)
const msg = extractErrorMessage(error)
toast.error(msg)
}
setIsClaiming(false)
}
return (
<>
<div className="w-11/12 text-foreground/60 text-center mx-auto">
You can claim $PEN every day. Dont miss out, claim todays $PEN airdrop
now.
</div>
{isLoading && <Skeleton className="h-9 w-32 mx-auto" />}
{data && (
<div className="flex items-center justify-center gap-2 h-9">
<div className="text-3xl font-bold text-center">{data.cap} $PEN</div>
<div className="text-foreground/50">claimable</div>
</div>
)}
<div className="flex items-center justify-center flex-1 -mt-10">
<Button
disabled={isClaiming}
size="lg"
className="w-40 flex gap-2"
onClick={claim}
>
{isClaiming ? (
<>
<div>Claiming</div>
<LoadingDots />
</>
) : (
'Claim $PEN'
)}
</Button>
</div>
</>
)
}

View File

@@ -17,7 +17,7 @@ export function ModeToggle() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="flex-shrink-0">
<Button variant="ghost" size="icon" className="flex-shrink-0 h-8 w-8">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>

View File

@@ -60,7 +60,7 @@ export function TipTokenDialog({
})
const { refetch: refetchTipStats } = useTipStats(receivers)
const { refetch: refetchTipInfo } = useTipInfo(post.id)
const { refetch: refetchTipInfo } = useTipInfo(post.id, !!params.slug)
const { data: data, isLoading: isLoadingCap, refetch } = useAllocationCap()
const { writeContractAsync } = useWriteContract()

View File

@@ -26,7 +26,8 @@ export function TippedAmount(props: Props) {
}
export function TippedAmountInPostDetail({ post, setIsOpen }: Props) {
const { data, isLoading } = useTipInfo(post.id)
const { data, isLoading } = useTipInfo(post.id, true)
if (isLoading) return <Skeleton className="h-6 w-12" />
return (
<div

20
hooks/useDailyClaimCap.ts Normal file
View File

@@ -0,0 +1,20 @@
import { DAILY_CLAIM_CAP_URL } from '@/lib/constants'
import { useQuery } from '@tanstack/react-query'
import ky from 'ky'
import { useAccount } from 'wagmi'
export function useDailyClaimCap() {
const { address } = useAccount()
const { data, ...rest } = useQuery({
queryKey: ['DailyClaimCap', address!],
async queryFn() {
return ky
.get(DAILY_CLAIM_CAP_URL + `?address=${address}`)
.json<{ cap: number }>()
},
enabled: !!address,
})
return { data, ...rest }
}

View File

@@ -8,7 +8,7 @@ type TipInfo = {
uri: string
}
export function useTipInfo(postId: string) {
export function useTipInfo(postId: string, enabled: boolean) {
const query = gql`
query geTip($postId: String!) {
tip(id: $postId) {
@@ -30,7 +30,7 @@ export function useTipInfo(postId: string) {
},
})
},
enabled: !!postId,
enabled: !!postId && enabled,
})
return {

View File

@@ -1,2 +1,2 @@
export * from './space'
export { penTokenAbi, tipAbi, creationFactoryAbi } from './penx'
export { penTokenAbi, tipAbi, dailyClaimAbi, creationFactoryAbi } from './penx'

View File

@@ -637,6 +637,247 @@ export const creationFactoryAbi = [
{ type: 'receive', stateMutability: 'payable' },
] as const
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// DailyClaim
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
export const dailyClaimAbi = [
{
type: 'constructor',
inputs: [
{ name: '_keeper', internalType: 'address', type: 'address' },
{ name: '_pen', internalType: 'contract PenToken', type: 'address' },
],
stateMutability: 'nonpayable',
},
{
type: 'error',
inputs: [{ name: 'value', internalType: 'uint256', type: 'uint256' }],
name: 'ExecutionFeeNotEnough',
},
{
type: 'error',
inputs: [{ name: 'requestId', internalType: 'uint256', type: 'uint256' }],
name: 'InvalidRequestId',
},
{ type: 'error', inputs: [], name: 'OnlyKeeper' },
{
type: 'error',
inputs: [{ name: 'owner', internalType: 'address', type: 'address' }],
name: 'OwnableInvalidOwner',
},
{
type: 'error',
inputs: [{ name: 'account', internalType: 'address', type: 'address' }],
name: 'OwnableUnauthorizedAccount',
},
{ type: 'error', inputs: [], name: 'ReentrancyGuardReentrantCall' },
{ type: 'error', inputs: [], name: 'TodayIsClaimed' },
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'requestId',
internalType: 'uint256',
type: 'uint256',
indexed: false,
},
{
name: 'account',
internalType: 'address',
type: 'address',
indexed: true,
},
],
name: 'ClaimRequestCreated',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'requestId',
internalType: 'uint256',
type: 'uint256',
indexed: false,
},
{
name: 'account',
internalType: 'address',
type: 'address',
indexed: true,
},
{
name: 'amount',
internalType: 'uint256',
type: 'uint256',
indexed: false,
},
],
name: 'ClaimRequestExecuted',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'newExecutionFee',
internalType: 'uint256',
type: 'uint256',
indexed: false,
},
],
name: 'ExecutionFeeUpdated',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'newKeeper',
internalType: 'address',
type: 'address',
indexed: false,
},
],
name: 'KeeperUpdated',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'previousOwner',
internalType: 'address',
type: 'address',
indexed: true,
},
{
name: 'newOwner',
internalType: 'address',
type: 'address',
indexed: true,
},
],
name: 'OwnershipTransferred',
},
{
type: 'function',
inputs: [{ name: '', internalType: 'address', type: 'address' }],
name: 'claimedTimes',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'createClaimRequest',
outputs: [{ name: 'requestId', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'payable',
},
{
type: 'function',
inputs: [
{ name: 'requestId', internalType: 'uint256', type: 'uint256' },
{ name: 'amount', internalType: 'uint256', type: 'uint256' },
],
name: 'executeClaimRequest',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [],
name: 'executionFee',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'getPendingRequests',
outputs: [
{
name: '',
internalType: 'struct DailyClaim.RequestInfo[]',
type: 'tuple[]',
components: [
{ name: 'requestId', internalType: 'uint256', type: 'uint256' },
{ name: 'user', internalType: 'address', type: 'address' },
],
},
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: 'requestId', internalType: 'uint256', type: 'uint256' }],
name: 'getRequest',
outputs: [
{
name: '',
internalType: 'struct DailyClaim.Request',
type: 'tuple',
components: [
{ name: 'user', internalType: 'address', type: 'address' },
],
},
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'keeper',
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'owner',
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'renounceOwnership',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [],
name: 'requestIndex',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: 'value', internalType: 'uint256', type: 'uint256' }],
name: 'setExecutionFee',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [{ name: '_keeper', internalType: 'address', type: 'address' }],
name: 'setKeeper',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [{ name: 'newOwner', internalType: 'address', type: 'address' }],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
},
{ type: 'receive', stateMutability: 'payable' },
] as const
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ECDSA
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -5,6 +5,7 @@ const baseSepoliaAddress = {
CreationFactory: '0xB2ebC5f85E0DA834CB71884150d2Fd738fEf918B',
PenToken: '0xd8501D1063Db721512572738e53775F11C05Df10',
Tip: '0xe31E51b20C1a054Fa46Dacd31C8a45ce3C97C834',
DailyClaim: '0xfa68A007EF07d55218d4d1f5910A3A526850d001',
}
const baseAddress = {
@@ -12,6 +13,7 @@ const baseAddress = {
CreationFactory: '0xB9563EBeDE644956FB4d8EFE40440bAeA8da342D',
PenToken: '0xadA2eA2D7e2AbB724F860ED8d08F85B25a4cB90d',
Tip: '0xD1B9751cdb3A6599f47eb3581446750c949c5f51',
DailyClaim: '0x7BdD1C56363B5D2480f3CC1Cd3b0A250d07DB288',
}
export const addressMap: Record<keyof typeof baseSepoliaAddress, any> =

View File

@@ -12,7 +12,6 @@ export const IPFS_ADD_URL = 'https://penx.io/api/ipfs-add'
// export const IPFS_ADD_URL = 'http://localhost:4000/api/ipfs-add'
export const IPFS_GATEWAY = 'https://ipfs-gateway.spaceprotocol.xyz'
export const ELECTRIC_BASE_URL = 'http://43.154.135.183:3000'
export const GOOGLE_DRIVE_OAUTH_REDIRECT_URI =
@@ -98,5 +97,12 @@ export const DATABASE_TOOLBAR_HEIGHT = 42
export const SIDEBAR_WIDTH = 240
export const ALLOCATION_CAP_URL =
NETWORK === NetworkNames.BASE
? 'https://penx.io/api/allocation-cap'
: 'https://sepolia.penx.io/api/allocation-cap'
export const ALLOCATION_CAP_URL = NETWORK === NetworkNames.BASE? 'https://penx.io/api/allocation-cap': 'https://sepolia.penx.io/api/allocation-cap'
export const DAILY_CLAIM_CAP_URL =
NETWORK === NetworkNames.BASE
? 'https://penx.io/api/daily-claim-cap'
: 'https://sepolia.penx.io/api/daily-claim-cap'

View File

@@ -29,7 +29,7 @@
"@fower/utils": "^2.1.1",
"@glideapps/glide-data-grid": "^6.0.3",
"@hookform/resolvers": "^3.9.0",
"@penxio/types": "^0.0.3",
"@penxio/types": "^0.0.4",
"@prisma/client": "^5.21.1",
"@privy-io/server-auth": "^1.15.0",
"@radix-ui/react-accordion": "^1.2.1",
@@ -137,7 +137,7 @@
"ky": "^1.5.0",
"lightweight-charts": "^4.2.0",
"lodash": "^4.17.21",
"lucide-react": "^0.454.0",
"lucide-react": "^0.456.0",
"mime": "^4.0.4",
"mitt": "^3.0.1",
"nanoid": "^5.0.8",
@@ -150,11 +150,11 @@
"octokit": "^4.0.2",
"openai-edge": "^1.2.2",
"p-retry": "^6.2.0",
"penx-theme-card": "^0.0.3",
"penx-theme-photo": "^0.0.3",
"penx-theme-micro": "^0.0.3",
"penx-theme-minimal": "^0.0.3",
"penx-theme-garden": "^0.0.3",
"penx-theme-card": "^0.0.4",
"penx-theme-photo": "^0.0.4",
"penx-theme-micro": "^0.0.4",
"penx-theme-minimal": "^0.0.4",
"penx-theme-garden": "^0.0.4",
"prisma-extension-pagination": "^0.7.4",
"prismjs": "^1.29.0",
"rc-table": "^7.48.1",

96
pnpm-lock.yaml generated
View File

@@ -57,8 +57,8 @@ importers:
specifier: ^3.9.0
version: 3.9.0(react-hook-form@7.53.1(react@18.2.0))
'@penxio/types':
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
'@prisma/client':
specifier: ^5.21.1
version: 5.21.1(prisma@5.21.1)
@@ -381,8 +381,8 @@ importers:
specifier: ^4.17.21
version: 4.17.21
lucide-react:
specifier: ^0.454.0
version: 0.454.0(react@18.2.0)
specifier: ^0.456.0
version: 0.456.0(react@18.2.0)
mime:
specifier: ^4.0.4
version: 4.0.4
@@ -420,20 +420,20 @@ importers:
specifier: ^6.2.0
version: 6.2.0
penx-theme-card:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
penx-theme-garden:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
penx-theme-micro:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
penx-theme-minimal:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
penx-theme-photo:
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.0.4
version: 0.0.4
prisma-extension-pagination:
specifier: ^0.7.4
version: 0.7.4(@prisma/client@5.21.1(prisma@5.21.1))
@@ -2234,11 +2234,11 @@ packages:
resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==}
engines: {node: '>= 10.0.0'}
'@penxio/types@0.0.3':
resolution: {integrity: sha512-utn5+iu8mnXVWk3XA3GlBY1e+knU9WF2PMIv+hbwvQgO3mI+ExogJ2rZV6OXgkxxRDzSwyL2Vib/WBNos5+AUw==}
'@penxio/types@0.0.4':
resolution: {integrity: sha512-E+T58oPqDmiMt4G7ivnt1ngWzeGhgOF87OD06QVQgz7B28hMcJChDRzvF01LkejUiVViBm8CfnvN5zroehmwtg==}
'@penxio/utils@0.0.3':
resolution: {integrity: sha512-pMrg6omLLk5rifQjkSJuCraiTd8TGDTL2W1y08GhPYQ0EB68dtTXm/d3KIvQmpoclSYZ7cWKUgkDUL+Onr+APg==}
'@penxio/utils@0.0.4':
resolution: {integrity: sha512-qD33j/si8KMFDeUaw4OURKi6wQcoX0uKKPaRqlWF+UOe8zoNyWiUXZRPHMhgcuHk1dlXas0jRXmr/pt5aTVeOA==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@@ -6605,8 +6605,8 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.454.0:
resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==}
lucide-react@0.456.0:
resolution: {integrity: sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
@@ -7417,20 +7417,20 @@ packages:
pathe@1.1.2:
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
penx-theme-card@0.0.3:
resolution: {integrity: sha512-UCua5ivXEudqAX0B9UZTlgXGDv1XkWByuNV27LkR6QQECzVR/aqRrk0CQEicKvRDq8uzknZojhMtklv/fPsXxA==}
penx-theme-card@0.0.4:
resolution: {integrity: sha512-MOHrV0oZlNC5YJBhK7ocrldLfpDmiFEJkQ5zNyPb14pgwPt4YaxDsmyhuVElvP1BK7owYPHh0E2MkTvWmGAwBA==}
penx-theme-garden@0.0.3:
resolution: {integrity: sha512-G/ouvhuoHRzUoULKBqKeHfy+ChfHMJlwzewdpyCbXairX1vEdeccCue7fdbbnUG+uiKpmc4nI6KRG/OmOdJT6g==}
penx-theme-garden@0.0.4:
resolution: {integrity: sha512-pUIoOZiIzrCb0iE5QUy6968piJHpfRcLr/UvT4QvHc7tFqNkfLaZO5ZwzMtskhT0i73oQFPn7VcRF0iCGkOjIg==}
penx-theme-micro@0.0.3:
resolution: {integrity: sha512-pVAUdkajMNh1jyhfr5Y3BdV5iKU1e7l6OiuCG5oWyY2oGyRpdLfkLoCca0kfsPJoh1oAjCP1Xm6YiXEYE/Jr5w==}
penx-theme-micro@0.0.4:
resolution: {integrity: sha512-HFGc6f0dumWcNIMPDGZWJWmZGfSz7BhYd7FgZm0LUui+1j0P+/w2w0VR0WBH0iEmP9+7bhnVgYTwDeGaZvhyKg==}
penx-theme-minimal@0.0.3:
resolution: {integrity: sha512-JXVd8aeAtOpQcrlFQ5sKQ9L191BxTZUF4kR1wIVSfUEvVJkeGkxFG2uZ8y5WG6fk1sm/Pv28juFnG4Gz0hqN1Q==}
penx-theme-minimal@0.0.4:
resolution: {integrity: sha512-6D6GYUN8Vdnll7iSuCKozRlOJWghCE5wCzuhJuh50GvPD9xVOxRzYUAqGjpH/FLdkFdUqlC2z0Vm1xWMGSWWOg==}
penx-theme-photo@0.0.3:
resolution: {integrity: sha512-yiQTxS5idyxhwzeR2EylGISULM6MQjPzE4X5IDZCRIRAg9N200TaD75d42htEbokM/ck32zoyOAv27y+/NZRJw==}
penx-theme-photo@0.0.4:
resolution: {integrity: sha512-Cvh5vtFPRmQsM0IfpU5LhQ3nA0MixEiFwyXPFdNxvjH3pmhwla0connGtICfAZE/1QURKp+KyJwuRShazRNaxw==}
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
@@ -11489,9 +11489,9 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.4.1
'@parcel/watcher-win32-x64': 2.4.1
'@penxio/types@0.0.3': {}
'@penxio/types@0.0.4': {}
'@penxio/utils@0.0.3':
'@penxio/utils@0.0.4':
dependencies:
clsx: 2.1.1
tailwind-merge: 2.5.4
@@ -16815,7 +16815,7 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-react@0.454.0(react@18.2.0):
lucide-react@0.456.0(react@18.2.0):
dependencies:
react: 18.2.0
@@ -17994,42 +17994,42 @@ snapshots:
pathe@1.1.2: {}
penx-theme-card@0.0.3:
penx-theme-card@0.0.4:
dependencies:
'@penxio/types': 0.0.3
'@penxio/utils': 0.0.3
'@penxio/types': 0.0.4
'@penxio/utils': 0.0.4
clsx: 2.1.1
github-slugger: 2.0.0
tailwind-merge: 2.5.4
penx-theme-garden@0.0.3:
penx-theme-garden@0.0.4:
dependencies:
'@penxio/types': 0.0.3
'@penxio/utils': 0.0.3
'@penxio/types': 0.0.4
'@penxio/utils': 0.0.4
clsx: 2.1.1
github-slugger: 2.0.0
tailwind-merge: 2.5.4
penx-theme-micro@0.0.3:
penx-theme-micro@0.0.4:
dependencies:
'@penxio/types': 0.0.3
'@penxio/utils': 0.0.3
'@penxio/types': 0.0.4
'@penxio/utils': 0.0.4
clsx: 2.1.1
github-slugger: 2.0.0
tailwind-merge: 2.5.4
penx-theme-minimal@0.0.3:
penx-theme-minimal@0.0.4:
dependencies:
'@penxio/types': 0.0.3
'@penxio/utils': 0.0.3
'@penxio/types': 0.0.4
'@penxio/utils': 0.0.4
clsx: 2.1.1
github-slugger: 2.0.0
tailwind-merge: 2.5.4
penx-theme-photo@0.0.3:
penx-theme-photo@0.0.4:
dependencies:
'@penxio/types': 0.0.3
'@penxio/utils': 0.0.3
'@penxio/types': 0.0.4
'@penxio/utils': 0.0.4
clsx: 2.1.1
github-slugger: 2.0.0
tailwind-merge: 2.5.4

View File

@@ -178,7 +178,7 @@ model Comment {
user User @relation(fields: [userId], references: [id])
userId String @db.Uuid
@@unique([userId, postId])
@@index([userId, postId])
@@index([postId])
@@index([userId])
@@map("comment")

View File

@@ -1,5 +1,6 @@
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
const crypto = require('crypto');
function genSecret(size = 32) {
const bytes = crypto.getRandomValues(new Uint8Array(size))