Skip to content

Commit

Permalink
Merge pull request #78 from besscroft/dev
Browse files Browse the repository at this point in the history
v0.8.5
  • Loading branch information
besscroft authored Jun 26, 2024
2 parents d8ee7a6 + 4e66d89 commit 8bbbc17
Show file tree
Hide file tree
Showing 17 changed files with 5,521 additions and 4,323 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js
### 功能特性

- 瀑布流相册展示图片,支持常见的格式。
- 点击图片查看原图,浏览图片信息和 EXIF 信息。
- 点击图片查看原图,浏览图片信息和 EXIF 信息,支持直链访问
- 响应式设计,在 PC 和移动端都有不错的体验,支持暗黑模式。
- 图片存储兼容 S3 API、Cloudflare R2、AList API。
- 图片支持绑定标签,并且可通过标签进行交互,筛选标签下所有图片。
Expand All @@ -43,7 +43,10 @@ PicImpact 是一个摄影师专用的摄影作品展示网站,基于 Next.js

> 部署就是这么简单,只需要您准备一个干净的数据库就行!
> 除了容器化部署方式外,其它的部署方式都需要执行 `pnpm run prisma:deploy` 来完成 prisma 迁移。
>
> 如果是 Vercel 部署,直接将 `Build Command` 设置为 `pnpm run build:vercel` 即可。
>
> 如果您自行使用 node 部署,请使用 `pnpm run build:node` 命令来构建。
### 容器化部署

Expand Down
2 changes: 1 addition & 1 deletion app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default async function Admin() {
<span className="pr-6">如果您有 Bug 反馈和建议</span>
<span className="h-px flex-1 bg-black"></span>
</span>
<Link href="https://github.com/besscroft/kamera/issues/new" target="_blank">
<Link href="https://github.com/besscroft/PicImpact/issues/new" target="_blank">
<Button startContent={<MessageSquareHeart size={20} />} variant="bordered" size="sm">反馈 | 建议</Button>
</Link>
</CardBody>
Expand Down
16 changes: 16 additions & 0 deletions app/api/open/get-image-by-id/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'server-only'
import { fetchImageByIdAndAuth } from '~/server/lib/query'
import { NextRequest } from 'next/server'

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
const data = await fetchImageByIdAndAuth(Number(id));
if (data && data?.length > 0) {
return Response.json({ code: 200, msg: '图片数据获取成功!', data: data })
} else {
return Response.json({ code: 500, message: '图片不存在或未公开展示!' })
}
}

export const dynamic = 'force-dynamic'
9 changes: 9 additions & 0 deletions app/api/v1/update-copyright-default/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'server-only'
import { updateCopyrightDefault } from '~/server/lib/operate'
import { NextRequest } from 'next/server'

export async function PUT(req: NextRequest) {
const copyright = await req.json()
const data = await updateCopyrightDefault(copyright.id, copyright.default);
return Response.json(data)
}
32 changes: 31 additions & 1 deletion components/Masonry.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
'use client'

import React from 'react'
import React, { useEffect } from 'react'
import { ImageHandleProps, ImageType } from '~/types'
import PhotoAlbum from 'react-photo-album'
import { Button, Image, Spinner } from '@nextui-org/react'
import { useSWRPageTotalHook } from '~/hooks/useSWRPageTotalHook'
import useSWRInfinite from 'swr/infinite'
import { useButtonStore } from '~/app/providers/button-store-Providers'
import MasonryItem from '~/components/MasonryItem'
import { toast } from 'sonner'
import { useSearchParams } from 'next/navigation'

export default function Masonry(props : Readonly<ImageHandleProps>) {
const { data: pageTotal } = useSWRPageTotalHook(props)
Expand All @@ -26,6 +28,34 @@ export default function Masonry(props : Readonly<ImageHandleProps>) {
const { setMasonryView, setMasonryViewData } = useButtonStore(
(state) => state,
)
const searchParams = useSearchParams()

useEffect(() => {
const fetchData = async (id: number) => {
try {
const res = await fetch(`/api/open/get-image-by-id?id=${id}`, {
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
}).then(response => response.json())
if (res.code == 200 && Array.isArray(res.data) && res.data?.length > 0) {
console.log(res)
setMasonryView(true)
setMasonryViewData(res.data[0])
} else {
toast.warning(res.message)
}
} catch (error) {
console.log(error)
toast.error('图片获取错误,请重试!')
}
};
const id = searchParams.get('id')
if (Number(id) > 0) {
fetchData(Number(id));
}
}, []);

return (
<div className="w-full sm:w-4/5 mx-auto p-2">
Expand Down
24 changes: 23 additions & 1 deletion components/MasonryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import {
import { useButtonStore } from '~/app/providers/button-store-Providers'
import { CopyrightType, DataProps, ImageType } from '~/types'
import { Image, Tabs, Tab, Card, CardHeader, CardBody, CardFooter, Button, Chip, Link, Avatar } from '@nextui-org/react'
import { Aperture, Camera, Image as ImageIcon, Languages, CalendarDays, X, SunMedium, MoonStar, Copyright, Crosshair, Timer, CircleGauge } from 'lucide-react'
import { Aperture, Camera, Image as ImageIcon, Languages, CalendarDays, X, SunMedium, MoonStar, Copyright, Crosshair, Timer, CircleGauge, Copy } from 'lucide-react'
import * as React from 'react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next-nprogress-bar'
import ExifView from '~/components/ExifView'
import { toast } from 'sonner'
import { usePathname } from 'next/navigation'

export default function MasonryItem() {
const router = useRouter()
const pathname = usePathname()
const { MasonryView, MasonryViewData, setMasonryView, setMasonryViewData } = useButtonStore(
(state) => state,
)
Expand All @@ -41,6 +44,25 @@ export default function MasonryItem() {
<p>{MasonryViewData.title}</p>
</div>
<div className="flex items-center space-x-4">
<Button
isIconOnly
variant="shadow"
size="sm"
aria-label="复制直链"
className="bg-white dark:bg-gray-800"
onClick={async () => {
try {
const url = window.location.origin + (pathname === '/' ? '/preview/' : pathname + '/preview/') + MasonryViewData.id
// @ts-ignore
await navigator.clipboard.writeText(url);
toast.success('复制直链成功!')
} catch (error) {
toast.error('复制直链失败!')
}
}}
>
<Copy size={20}/>
</Button>
<Button
isIconOnly
variant="shadow"
Expand Down
30 changes: 30 additions & 0 deletions components/admin/copyright/CopyrightEditSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,36 @@ export default function CopyrightEditSheet(props : Readonly<HandleProps>) {
</p>
</div>
</Switch>
<Switch
isSelected={copyright?.default === 0}
value={copyright?.default === 0 ? 'true' : 'false'}
onValueChange={(value) => {
setCopyrightEditData({ ...copyright, default: value ? 0 : 1 })
}}
classNames={{
base: cn(
"inline-flex flex-row-reverse w-full max-w-full bg-content1 hover:bg-content2 items-center",
"justify-between cursor-pointer rounded-lg gap-2 p-4 border-2 border-transparent",
"data-[selected=true]:border-primary",
),
wrapper: "p-0 h-4 overflow-visible",
thumb: cn("w-6 h-6 border-2 shadow-lg",
"group-data-[hover=true]:border-primary",
//selected
"group-data-[selected=true]:ml-6",
// pressed
"group-data-[pressed=true]:w-7",
"group-data-[selected]:group-data-[pressed]:ml-4",
),
}}
>
<div className="flex flex-col gap-1">
<p className="text-medium">默认状态</p>
<p className="text-tiny text-default-400">
设置为默认后,所有的图片都会默认显示该版权信息。
</p>
</div>
</Switch>
<Button
isLoading={loading}
color="primary"
Expand Down
52 changes: 50 additions & 2 deletions components/admin/copyright/CopyrightList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
Spinner, Switch
Spinner,
Switch,
} from '@nextui-org/react'
import { Eye, EyeOff, Pencil, Trash } from 'lucide-react'
import { Eye, EyeOff, Pencil, Trash, BadgeCheck, BadgeX } from 'lucide-react'
import { useButtonStore } from '~/app/providers/button-store-Providers'

export default function CopyrightList(props : Readonly<HandleProps>) {
Expand All @@ -32,6 +33,8 @@ export default function CopyrightList(props : Readonly<HandleProps>) {
const [deleteLoading, setDeleteLoading] = useState(false)
const [updateCopyrightLoading, setUpdateCopyrightLoading] = useState(false)
const [updateCopyrightId, setUpdateCopyrightId] = useState(0)
const [updateCopyrightDefaultLoading, setUpdateCopyrightDefaultLoading] = useState(false)
const [updateCopyrightDefaultId, setUpdateCopyrightDefaultId] = useState(0)
const { setCopyrightEdit, setCopyrightEditData } = useButtonStore(
(state) => state,
)
Expand Down Expand Up @@ -85,6 +88,34 @@ export default function CopyrightList(props : Readonly<HandleProps>) {
}
}

async function updateCopyrightDefault(id: number, defaultValue: number) {
try {
setUpdateCopyrightDefaultId(id)
setUpdateCopyrightDefaultLoading(true)
const res = await fetch(`/api/v1/update-copyright-default`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id,
default: defaultValue
}),
})
if (res.status === 200) {
toast.success('更新成功!')
await mutate()
} else {
toast.error('更新失败!')
}
} catch (e) {
toast.error('更新失败!')
} finally {
setUpdateCopyrightDefaultId(0)
setUpdateCopyrightDefaultLoading(false)
}
}

return (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
Expand Down Expand Up @@ -136,6 +167,23 @@ export default function CopyrightList(props : Readonly<HandleProps>) {
onValueChange={(isSelected: boolean) => updateCopyrightShow(copyright.id, isSelected ? 0 : 1)}
/>
}
{updateCopyrightDefaultLoading && updateCopyrightDefaultId === copyright.id ? <Spinner size="sm" /> :
<Switch
defaultSelected
size="sm"
color="success"
isSelected={copyright.default === 0}
isDisabled={updateCopyrightDefaultLoading}
thumbIcon={({ isSelected }) =>
isSelected ? (
<BadgeCheck size={20} />
) : (
<BadgeX size={20} />
)
}
onValueChange={(isSelected: boolean) => updateCopyrightDefault(copyright.id, isSelected ? 0 : 1)}
/>
}
</div>
<div className="space-x-1">
<Button
Expand Down
4 changes: 2 additions & 2 deletions components/layout/DropMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const DropMenu = () => {
<DropdownItem
key="github"
startContent={<Github size={20} className={iconClasses} />}
onClick={() => router.push('https://github.com/besscroft')}
onClick={() => router.push('https://github.com/besscroft/PicImpact')}
>
GitHub
</DropdownItem>
Expand All @@ -109,7 +109,7 @@ export const DropMenu = () => {
<DropdownItem
key="github"
startContent={<Github size={20} className={iconClasses} />}
onClick={() => router.push('https://github.com/besscroft')}
onClick={() => router.push('https://github.com/besscroft/PicImpact')}
>
GitHub
</DropdownItem>
Expand Down
16 changes: 15 additions & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { auth } from '~/server/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {

if (req.nextUrl.pathname.startsWith('/api/v1') && !req.auth) {
return Response.json(
{ success: false, message: 'authentication failed' },
Expand All @@ -15,6 +14,21 @@ export default auth((req) => {
if (req.auth && req.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/', req.url))
}
if (req.nextUrl.pathname.includes('/preview/')) {
const startIndex = req.nextUrl.pathname.indexOf('/preview/') + '/preview/'.length;
const contentAfterPreview = req.nextUrl.pathname.substring(startIndex);
if (req.nextUrl.pathname.startsWith('/preview')) {
const redirectUrl = new URL('/', req.url)
redirectUrl.searchParams.set('id', String(contentAfterPreview))
return NextResponse.redirect(redirectUrl)
} else {
let endIndex = req.nextUrl.pathname.indexOf('/preview');
let contentBeforePreview = req.nextUrl.pathname.substring(0, endIndex);
const redirectUrl = new URL(contentBeforePreview, req.url)
redirectUrl.searchParams.set('id', String(contentAfterPreview))
return NextResponse.redirect(redirectUrl)
}
}
});

// Optionally, don't invoke Middleware on some paths
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "pnpm run prisma:generate && next dev",
"build": "next build",
"build:vercel": "pnpm run prisma:deploy && next build",
"build:node": "pnpm run prisma:deploy && next build",
"start": "next start",
"lint": "next lint",
"prisma:format": "npx prisma format",
Expand Down
Loading

0 comments on commit 8bbbc17

Please sign in to comment.