Skip to content

Commit fb46804

Browse files
committed
feat: post pin state
1 parent 834dd6a commit fb46804

File tree

13 files changed

+213
-46
lines changed

13 files changed

+213
-46
lines changed

.env

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
NEXT_PUBLIC_API_URL=http://127.0.0.1:2333/api/v2
2+
NEXT_PUBLIC_GATEWAY_URL=http://127.0.0.1:2333/
3+
NEXT_PUBLIC_API_URL=http://jxhome.shizuri.net:52333/api/v2
4+
NEXT_PUBLIC_GATEWAY_URL=http://jxhome.shizuri.net:52333
5+
# NEXT_PUBLIC_API_URL=http://10.0.0.33:2333/api/v2
6+
# NEXT_PUBLIC_GATEWAY_URL=http://10.0.0.33:2333
7+
# NEXT_PUBLIC_API_URL=https://innei.ren/api/v2
8+
# NEXT_PUBLIC_GATEWAY_URL=https://api.innei.ren
9+
10+
SENTRY=true
11+
NEXT_PUBLIC_SENTRY_DSN=https://f7660879c3c645e99e9c040aef4072ba@o4505266479366144.ingest.sentry.io/4505339474870272
12+
NEXT_PUBLIC_SENTRY_AUTH_TOKEN=7c9c58e200f6488791669ab910cf168f685c46fd179240d79630d8e7ba233030
13+
14+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cmVzb2x2ZWQtd2FydGhvZy0yNC5jbGVyay5hY2NvdW50cy5kZXYk
15+
CLERK_SECRET_KEY=sk_test_qOKIV9c9sIwzWP4SPhcOk5BDBfHWoG3nqZ54vzES8q
16+
17+
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
18+
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
19+
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
20+
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
21+
22+
# vercel kv
23+
KV_URL="redis://default:876b3dd5d7404a7b86e94129d710787b@choice-lark-39044.kv.vercel-storage.com:39044"
24+
KV_REST_API_URL="https://choice-lark-39044.kv.vercel-storage.com"
25+
KV_REST_API_TOKEN="AZiEASQgOWM3YjZmMjgtN2IxNy00OGQwLWFjMGUtZDdmNzA0MDdiNzllODc2YjNkZDVkNzQwNGE3Yjg2ZTk0MTI5ZDcxMDc4N2I="
26+
KV_REST_API_READ_ONLY_TOKEN="ApiEASQgOWM3YjZmMjgtN2IxNy00OGQwLWFjMGUtZDdmNzA0MDdiNzllIIQjhLMp8gB0yAMfPxP_S2Mgg9lrlahMtOg3XzQA5ys="

.gitignore

+1-36
Original file line numberDiff line numberDiff line change
@@ -1,36 +1 @@
1-
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2-
3-
# dependencies
4-
/node_modules
5-
6-
# testing
7-
/coverage
8-
9-
# next.js
10-
/.next/
11-
/out/
12-
13-
# production
14-
/build
15-
16-
# misc
17-
.DS_Store
18-
*.pem
19-
20-
# debug
21-
npm-debug.log*
22-
yarn-debug.log*
23-
yarn-error.log*
24-
.pnpm-debug.log*
25-
26-
# local env files
27-
.env*.local
28-
29-
# vercel
30-
.vercel
31-
32-
# typescript
33-
*.tsbuildinfo
34-
next-env.d.ts
35-
36-
.env
1+
.idea

next-env.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.

src/app/login/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useState } from 'react'
44
import { useRouter } from 'next/navigation'
55

6-
import { login } from '~/atoms/owner'
76
import { StyledButton } from '~/components/ui/button'
87
import { Input } from '~/components/ui/input/Input'
98
import { Routes } from '~/lib/route-builder'
@@ -13,8 +12,9 @@ export default () => {
1312
const [password, setPassword] = useState('')
1413
const router = useRouter()
1514

16-
const handleLogin = (e: any) => {
15+
const handleLogin = async (e: any) => {
1716
e.preventDefault()
17+
const { login } = await import('~/atoms/owner')
1818
login(username, password).then(() => {
1919
router.push(Routes.Home)
2020
})

src/atoms/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export * from './online'
22
export * from './viewport'
3+
export * from './css-media'
4+
export * from './owner'
5+
export * from './url'

src/atoms/owner.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const login = async (username?: string, password?: string) => {
2626
toast(`欢迎回来,${jotaiStore.get(ownerAtom)?.name}`, 'success')
2727
}
2828

29-
return Promise.resolve()
29+
return true
3030
}
3131

3232
const token = getToken()
@@ -49,11 +49,15 @@ export const login = async (username?: string, password?: string) => {
4949
return
5050
}
5151

52-
apiClient.user.proxy.login.put<{ token: string }>().then((res) => {
52+
await apiClient.user.proxy.login.put<{ token: string }>().then((res) => {
5353
jotaiStore.set(isLoggedAtom, true)
5454
toast(`欢迎回来,${jotaiStore.get(ownerAtom)?.name}`, 'success')
5555
setToken(res.token)
5656
})
57+
58+
return true
5759
}
5860

5961
export const useIsLogged = () => useAtomValue(isLoggedAtom)
62+
63+
export const isLogged = () => jotaiStore.get(isLoggedAtom)

src/atoms/url.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { atom, useAtomValue } from 'jotai'
2+
3+
import { jotaiStore } from '~/lib/store'
4+
import { apiClient } from '~/utils/request'
5+
6+
export interface UrlConfig {
7+
adminUrl: string
8+
backendUrl: string
9+
10+
frontendUrl: string
11+
}
12+
13+
const appUrlAtom = atom<UrlConfig | null>(null)
14+
15+
export const fetchAppUrl = async () => {
16+
const { data } = await apiClient.proxy.options.url.get<{
17+
data: UrlConfig
18+
}>()
19+
20+
jotaiStore.set(appUrlAtom, data)
21+
}
22+
23+
export const getAppUrl = () => jotaiStore.get(appUrlAtom)
24+
export const useAppUrl = () => useAtomValue(appUrlAtom)

src/components/layout/header/internal/AnimatedLogo.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import { AnimatePresence, motion } from 'framer-motion'
44
import { useRouter } from 'next/navigation'
55

6-
import { useViewport } from '~/atoms'
6+
import { getAppUrl, isLogged, useViewport } from '~/atoms'
77
import { useSingleAndDoubleClick } from '~/hooks/common/use-single-double-click'
88
import { Routes } from '~/lib/route-builder'
9+
import { toast } from '~/lib/toast'
910

1011
import { useHeaderMetaShouldShow } from './hooks'
1112
import { Logo } from './Logo'
@@ -17,6 +18,14 @@ const TapableLogo = () => {
1718
router.push(Routes.Home)
1819
},
1920
() => {
21+
if (isLogged()) {
22+
const adminUrl = getAppUrl()?.adminUrl
23+
if (adminUrl) location.href = adminUrl
24+
else {
25+
toast('Admin url not found', 'error')
26+
}
27+
return
28+
}
2029
router.push(Routes.Login)
2130
},
2231
)

src/components/layout/header/internal/SiteOwnerAvatar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const SiteOwnerAvatar: Component = ({ className }) => {
1212
return (
1313
<div
1414
className={clsxm(
15-
'overflow-hidden rounded-md border-[1.5px] border-slate-300 dark:border-neutral-800',
15+
'pointer-events-none select-none overflow-hidden rounded-md border-[1.5px] border-slate-300 dark:border-neutral-800',
1616
className,
1717
)}
1818
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client'
2+
3+
import { useAnimationControls } from 'framer-motion'
4+
import type { FC } from 'react'
5+
import React, { useEffect, useState } from 'react'
6+
import {FadeInOutTransitionView} from "~/components/ui/transition/FadeInOutTransitionView";
7+
8+
9+
interface IconTransitionProps {
10+
solidIcon: JSX.Element
11+
regularIcon: JSX.Element
12+
currentState: 'solid' | 'regular'
13+
}
14+
export const IconTransition: FC<IconTransitionProps> = (props) => {
15+
const { currentState, regularIcon, solidIcon } = props
16+
17+
const map = {
18+
solid: solidIcon,
19+
regular: regularIcon,
20+
}
21+
const [currentIcon, setCurrentIcon] = useState(map[currentState])
22+
const controls = useAnimationControls()
23+
24+
useEffect(() => {
25+
controls.start({ opacity: 0 }).then(() => {
26+
setCurrentIcon(map[currentState])
27+
requestAnimationFrame(() => {
28+
controls.start({ opacity: 1 })
29+
})
30+
})
31+
}, [currentState])
32+
33+
return (
34+
<FadeInOutTransitionView
35+
initial
36+
animate={controls}
37+
transition={{ duration: 0.2 }}
38+
key={currentState}
39+
>
40+
{currentIcon}
41+
</FadeInOutTransitionView>
42+
)
43+
}

src/components/widgets/post/PostItem.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IcRoundKeyboardDoubleArrowRight } from '~/components/icons/arrow'
99
import { MdiClockOutline } from '~/components/icons/clock'
1010
import { FeHash } from '~/components/icons/fa-hash'
1111
import { RelativeTime } from '~/components/ui/relative-time'
12+
import { PostPinIcon } from '~/components/widgets/post/PostPinIcon'
1213

1314
import { PostItemHoverOverlay } from './PostItemHoverOverlay'
1415

@@ -27,8 +28,10 @@ export const PostItem = memo<{ data: PostModel }>(({ data }) => {
2728
className="relative flex flex-col space-y-2 py-6 focus-visible:!shadow-none"
2829
>
2930
<PostItemHoverOverlay />
30-
<h2 className="text-2xl font-medium">
31+
<h2 className="relative text-2xl font-medium">
3132
<Balancer>{data.title}</Balancer>
33+
34+
<PostPinIcon pin={!!data.pin} id={data.id} />
3235
</h2>
3336
{!!data.summary && (
3437
<p className="break-all leading-relaxed text-gray-900 dark:text-slate-50">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client'
2+
3+
import { memo, useState } from 'react'
4+
import type { MouseEventHandler, SVGProps } from 'react'
5+
6+
import { useIsLogged } from '~/atoms'
7+
import { MotionButtonBase } from '~/components/ui/button'
8+
import { IconTransition } from '~/components/ui/transition/IconTransiton'
9+
import { clsxm } from '~/utils/helper'
10+
import { apiClient } from '~/utils/request'
11+
12+
export const PostPinIcon = memo(({ pin, id }: { pin: boolean; id: string }) => {
13+
const isLogged = useIsLogged()
14+
const [pinState, setPinState] = useState(pin)
15+
const handlePin: MouseEventHandler<HTMLButtonElement> = async (e) => {
16+
e.preventDefault()
17+
e.stopPropagation()
18+
19+
await apiClient.post.proxy(id).patch({
20+
data: {
21+
pin: !pinState,
22+
},
23+
})
24+
25+
setPinState(!pinState)
26+
}
27+
return (
28+
<MotionButtonBase
29+
className={clsxm(
30+
'absolute bottom-0 right-0 top-0 z-[10] -m-4 box-content hidden h-5 w-5 items-center p-4',
31+
isLogged && 'inline-flex cursor-pointer',
32+
!isLogged && pinState && 'pointer-events-none',
33+
pinState && '!inline-flex text-uk-red-light',
34+
)}
35+
onClick={handlePin}
36+
>
37+
<IconTransition
38+
currentState={pinState ? 'solid' : 'regular'}
39+
regularIcon={<PhPushPin />}
40+
solidIcon={<PhPushPinFill />}
41+
/>
42+
</MotionButtonBase>
43+
)
44+
})
45+
function PhPushPinFill(props: SVGProps<SVGSVGElement>) {
46+
return (
47+
<svg
48+
xmlns="http://www.w3.org/2000/svg"
49+
width="1em"
50+
height="1em"
51+
viewBox="0 0 256 256"
52+
{...props}
53+
>
54+
<path
55+
fill="currentColor"
56+
d="m232 107.3l-58.5 58.5c4.5 12.7 6.4 33.9-13.2 60a16.3 16.3 0 0 1-11.7 6.4h-1.1a16.1 16.1 0 0 1-11.3-4.7L88 179.3l-34.3 34.4a8.2 8.2 0 0 1-11.4 0a8.1 8.1 0 0 1 0-11.4L76.7 168l-48.4-48.4a15.9 15.9 0 0 1 1.3-23.8C55 75.3 79.3 79.4 90 82.7L148.7 24a16.1 16.1 0 0 1 22.6 0L232 84.7a15.9 15.9 0 0 1 0 22.6Z"
57+
/>
58+
</svg>
59+
)
60+
}
61+
62+
function PhPushPin(props: SVGProps<SVGSVGElement>) {
63+
return (
64+
<svg
65+
xmlns="http://www.w3.org/2000/svg"
66+
width="1em"
67+
height="1em"
68+
viewBox="0 0 256 256"
69+
{...props}
70+
>
71+
<path
72+
fill="currentColor"
73+
d="M236.7 96a15.9 15.9 0 0 0-4.7-11.3L171.3 24a16.1 16.1 0 0 0-22.6 0L90 82.7c-10.7-3.3-35-7.4-60.4 13.1a15.9 15.9 0 0 0-1.3 23.8L76.7 168l-34.4 34.3a8.1 8.1 0 0 0 0 11.4a8.2 8.2 0 0 0 11.4 0L88 179.3l48.2 48.2a16.1 16.1 0 0 0 11.3 4.7h1.1a16.3 16.3 0 0 0 11.7-6.4c19.6-26.1 17.7-47.3 13.2-60l58.5-58.5a15.9 15.9 0 0 0 4.7-11.3Zm-78.4 62.3a8.2 8.2 0 0 0-1.5 9.3c9.5 18.9-1.8 38.6-9.3 48.6L39.6 108.3C51.7 98.5 63.3 96 72.1 96s15.9 2.9 16.3 3.2a8.2 8.2 0 0 0 9.3-1.5L160 35.3L220.7 96Z"
74+
/>
75+
</svg>
76+
)
77+
}

src/providers/root/aggregation-data-provider.tsx

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useCallback, useEffect } from 'react'
1+
import { useCallback, useEffect, useRef } from 'react'
22
import { atom, useAtomValue } from 'jotai'
33
import { selectAtom } from 'jotai/utils'
44
import type { AggregateRoot } from '@mx-space/api-client'
55
import type { FC, PropsWithChildren } from 'react'
66

7+
import { fetchAppUrl, isLogged } from '~/atoms'
78
import { login } from '~/atoms/owner'
89
import { useAggregationQuery } from '~/hooks/data/use-aggregation'
910
import { jotaiStore } from '~/lib/store'
@@ -18,9 +19,16 @@ export const AggregationProvider: FC<PropsWithChildren> = ({ children }) => {
1819
jotaiStore.set(aggregationDataAtom, data)
1920
}, [data])
2021

22+
const callOnceRef = useRef(false)
2123
useEffect(() => {
22-
login()
23-
}, [])
24+
if (callOnceRef.current) return
25+
if (!data?.user) return
26+
login().then(() => {
27+
console.log(isLogged())
28+
callOnceRef.current = true
29+
return fetchAppUrl()
30+
})
31+
}, [data?.user])
2432

2533
return children
2634
}

0 commit comments

Comments
 (0)