mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-30 01:36:49 +00:00
feat: implement email notification system for comments
- Added MailModule to handle email notifications for comment events. - Introduced CommentCreatedEvent to encapsulate comment creation details. - Implemented CommentNotificationListener to send notifications to relevant users when a comment is created. - Integrated Resend service for sending emails, with templates for comment notifications. - Updated various modules to support the new email notification feature. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
27
apps/web/src/lib/autolink.tsx
Normal file
27
apps/web/src/lib/autolink.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export const autolink = (text: string): ReactNode[] => {
|
||||
if (!text) return []
|
||||
const urlPattern = /((?:https?:\/\/|www\.)[\x21-\x7E]+)/g
|
||||
return text.split(urlPattern).map((part, i) => {
|
||||
if (urlPattern.test(part)) {
|
||||
let href = part
|
||||
if (part.startsWith('www.')) {
|
||||
href = `http://${part}`
|
||||
}
|
||||
return (
|
||||
<a
|
||||
key={i}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-400 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{part}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
return part
|
||||
})
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export const ExifPanelContent: FC<ExifPanelContentProps> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea rootClassName={rootClassName} viewportClassName={viewportClassName}>
|
||||
<ScrollArea mask rootClassName={rootClassName} viewportClassName={viewportClassName}>
|
||||
<div className={`space-y-${isMobile ? '3' : '4'}`}>
|
||||
{/* 基本信息和标签 - 合并到一个 section */}
|
||||
<div>
|
||||
|
||||
@@ -64,9 +64,9 @@ export const CommentItem = memo(({ comment, reacted, locale }: CommentItemProps)
|
||||
// isNew && 'animate-highlight-new',
|
||||
)}
|
||||
>
|
||||
<div className="relative z-10 flex gap-3">
|
||||
<div className="relative z-10 flex min-w-0 flex-row gap-3">
|
||||
<UserAvatar image={userImage} name={userName ?? comment.userId} fallback="?" size={36} />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex min-w-0 flex-1 flex-col space-y-2">
|
||||
<CommentHeader comment={comment} author={authorName(comment)} locale={locale} />
|
||||
<CommentContent comment={comment} parentId={comment.parentId} authorName={authorName} />
|
||||
<CommentActionBar reacted={reacted} reactionCount={comment.reactionCounts.like ?? 0} comment={comment} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMemo } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
import type { Comment } from '~/lib/api/comments'
|
||||
import { autolink } from '~/lib/autolink'
|
||||
|
||||
import { useCommentsContext } from './context'
|
||||
|
||||
@@ -24,7 +25,7 @@ export const CommentContent = ({ comment, parentId, authorName }: CommentContent
|
||||
return (
|
||||
<>
|
||||
{parent ? (
|
||||
<div className="rounded-lg border border-white/5 bg-white/5 px-3 py-2 text-xs text-white/70">
|
||||
<div className="flex min-w-0 flex-col rounded-lg border border-white/5 bg-white/5 px-3 py-2 text-xs text-white/70">
|
||||
<div className="mb-1 flex items-center text-[11px] tracking-wide text-white/40 uppercase">
|
||||
<i className="i-lucide-reply mr-2" />
|
||||
|
||||
@@ -34,11 +35,13 @@ export const CommentContent = ({ comment, parentId, authorName }: CommentContent
|
||||
values={{ user: authorName(parent) }}
|
||||
/>
|
||||
</div>
|
||||
<p className="line-clamp-3 text-sm leading-relaxed text-white/70">{parent.content}</p>
|
||||
<p className="line-clamp-3 text-sm leading-relaxed wrap-break-word text-white/70">
|
||||
{autolink(parent.content)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="text-sm leading-relaxed text-white/85">{comment.content}</p>
|
||||
<p className="text-sm leading-relaxed wrap-break-word text-white/85">{autolink(comment.content)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ export const CommentInput = () => {
|
||||
)}
|
||||
|
||||
{replyTo && !submitError ? (
|
||||
<div className="border-accent/20 bg-accent/5 mb-3 flex items-center justify-between rounded-lg border px-3 py-2 text-xs text-white/80 select-none">
|
||||
<div className="border-accent/20 bg-accent/50 mb-3 flex items-center justify-between rounded-lg border px-3 py-2 text-xs text-white/80 select-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-lucide-reply text-accent" />
|
||||
<i className="i-lucide-reply opacity-50" />
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey="comments.replyingTo"
|
||||
@@ -117,7 +117,6 @@ export const CommentInput = () => {
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
<p className="mt-2 text-xs text-white/40">{t('comments.hint')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ const CommentsContent: FC = () => {
|
||||
const sessionUser = useAtomValue(sessionUserAtom)
|
||||
return (
|
||||
<div className="flex min-h-0 w-full flex-1 flex-col">
|
||||
<ScrollArea rootClassName="flex-1 min-h-0" viewportClassName="px-4">
|
||||
<ScrollArea mask rootClassName="flex-1 min-h-0" viewportClassName="px-4">
|
||||
<div className="space-y-4 pb-4">
|
||||
{status.isLoading ? (
|
||||
{status.isLoading && !status.isLoadingMore && !status.isError && comments.length === 0 ? (
|
||||
<SkeletonList />
|
||||
) : status.isError ? (
|
||||
<ErrorBox />
|
||||
|
||||
@@ -35,17 +35,20 @@
|
||||
"better-auth": "1.4.2",
|
||||
"busboy": "1.6.0",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"ejs": "3.1.10",
|
||||
"hono": "4.10.7",
|
||||
"linkedom": "0.18.12",
|
||||
"mime-types": "3.0.2",
|
||||
"pg": "^8.16.3",
|
||||
"picocolors": "1.1.1",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"resend": "6.5.2",
|
||||
"satori": "0.18.3",
|
||||
"tsyringe": "4.10.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "3.1.5",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/pg": "8.15.6",
|
||||
|
||||
@@ -7,5 +7,6 @@ import { SystemSettingStore } from './system-setting.store.service'
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
providers: [SystemSettingStore, SystemSettingService],
|
||||
exports: [SystemSettingService],
|
||||
})
|
||||
export class SystemSettingModule {}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { authUsers, commentReactions, comments, photoAssets } from '@afilmory/db'
|
||||
import { HttpContext } from '@afilmory/framework'
|
||||
import { EventEmitterService, HttpContext } from '@afilmory/framework'
|
||||
import { getClientIp } from 'core/context/http-context.helper'
|
||||
import { DbAccessor } from 'core/database/database.provider'
|
||||
import { BizException, ErrorCode } from 'core/errors'
|
||||
import { logger } from 'core/helpers/logger.helper'
|
||||
import { CommentCreatedEvent } from 'core/modules/content/comment/events/comment-created.event'
|
||||
import { requireTenantContext } from 'core/modules/platform/tenant/tenant.context'
|
||||
import { and, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm'
|
||||
import type { Context } from 'hono'
|
||||
@@ -44,6 +46,7 @@ export class CommentService {
|
||||
constructor(
|
||||
private readonly dbAccessor: DbAccessor,
|
||||
@inject(COMMENT_MODERATION_HOOK) private readonly moderationHook: CommentModerationHook,
|
||||
private readonly eventEmitter: EventEmitterService,
|
||||
) {}
|
||||
|
||||
async createComment(
|
||||
@@ -152,6 +155,24 @@ export class CommentService {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit event asynchronously
|
||||
this.eventEmitter
|
||||
.emit(
|
||||
'comment.created',
|
||||
new CommentCreatedEvent(
|
||||
record.id,
|
||||
tenant.tenant.id,
|
||||
dto.photoId,
|
||||
auth.userId,
|
||||
parent?.id ?? null,
|
||||
dto.content.trim(),
|
||||
record.createdAt,
|
||||
),
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error('Failed to emit comment.created event', error)
|
||||
})
|
||||
|
||||
return { comments: [item], relations, users }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export class CommentCreatedEvent {
|
||||
constructor(
|
||||
public readonly commentId: string,
|
||||
public readonly tenantId: string,
|
||||
public readonly photoId: string,
|
||||
public readonly userId: string,
|
||||
public readonly parentId: string | null,
|
||||
public readonly content: string,
|
||||
public readonly createdAt: string,
|
||||
) {}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { CacheModule } from './infrastructure/cache/cache.module'
|
||||
import { DataSyncModule } from './infrastructure/data-sync/data-sync.module'
|
||||
import { HealthModule } from './infrastructure/health/health.module'
|
||||
import { StaticWebModule } from './infrastructure/static-web/static-web.module'
|
||||
import { MailModule } from './mail/mail.module'
|
||||
import { AuthModule } from './platform/auth/auth.module'
|
||||
import { BillingModule } from './platform/billing/billing.module'
|
||||
import { DashboardModule } from './platform/dashboard/dashboard.module'
|
||||
@@ -49,6 +50,7 @@ function createEventModuleOptions(redis: RedisAccessor) {
|
||||
inject: [RedisAccessor],
|
||||
}),
|
||||
RedisModule,
|
||||
MailModule,
|
||||
AuthModule,
|
||||
CacheModule,
|
||||
HealthModule,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { authUsers, comments } from '@afilmory/db'
|
||||
import { createLogger, OnEvent } from '@afilmory/framework'
|
||||
import { DbAccessor } from 'core/database/database.provider'
|
||||
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
|
||||
import { CommentCreatedEvent } from 'core/modules/content/comment/events/comment-created.event'
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import { injectable } from 'tsyringe'
|
||||
|
||||
import { MailService, TEMPLATES } from '../mail.service'
|
||||
|
||||
@injectable()
|
||||
export class CommentNotificationListener {
|
||||
private readonly logger = createLogger('CommentNotificationListener')
|
||||
constructor(
|
||||
private readonly dbAccessor: DbAccessor,
|
||||
private readonly mailService: MailService,
|
||||
private readonly systemSettingService: SystemSettingService,
|
||||
) {
|
||||
this.logger.info('CommentNotificationListener initialized')
|
||||
}
|
||||
|
||||
@OnEvent('comment.created')
|
||||
async handleCommentCreated(event: CommentCreatedEvent) {
|
||||
try {
|
||||
this.logger.verbose('Sending notifications for comment.created event', event)
|
||||
await this.sendNotifications(event)
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to handle comment.created event', error)
|
||||
}
|
||||
}
|
||||
|
||||
private async sendNotifications(event: CommentCreatedEvent) {
|
||||
const db = this.dbAccessor.get()
|
||||
const settings = await this.systemSettingService.getSettings()
|
||||
const { baseDomain } = settings
|
||||
// Assume https for now, or we could make protocol configurable if needed.
|
||||
// But typically production runs on https.
|
||||
const photoUrl = `https://${baseDomain}/photos/${event.photoId}`
|
||||
|
||||
const sentEmails = new Set<string>()
|
||||
|
||||
// Fetch commenter info
|
||||
const [commenter] = await db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
name: authUsers.name,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, event.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!commenter) {
|
||||
this.logger.warn(`Commenter ${event.userId} not found for notification`)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Notify parent comment author (Reply)
|
||||
if (event.parentId) {
|
||||
const [parent] = await db
|
||||
.select({
|
||||
id: comments.id,
|
||||
userId: comments.userId,
|
||||
})
|
||||
.from(comments)
|
||||
.where(eq(comments.id, event.parentId))
|
||||
.limit(1)
|
||||
|
||||
if (parent) {
|
||||
const [parentAuthor] = await db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
name: authUsers.name,
|
||||
email: authUsers.email,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, parent.userId))
|
||||
.limit(1)
|
||||
|
||||
if (parentAuthor && parentAuthor.id !== event.userId && parentAuthor.email) {
|
||||
await this.mailService.sendTemplate(
|
||||
parentAuthor.email,
|
||||
'New reply to your comment',
|
||||
TEMPLATES.commentNotification,
|
||||
{
|
||||
userName: commenter.name,
|
||||
content: event.content,
|
||||
photoUrl,
|
||||
replyToUser: parentAuthor.name,
|
||||
photoId: event.photoId,
|
||||
},
|
||||
)
|
||||
sentEmails.add(parentAuthor.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Notify Tenant Admins (Owner)
|
||||
const admins = await db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
name: authUsers.name,
|
||||
email: authUsers.email,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(
|
||||
and(eq(authUsers.tenantId, event.tenantId), or(eq(authUsers.role, 'admin'), eq(authUsers.role, 'superadmin'))),
|
||||
)
|
||||
|
||||
for (const admin of admins) {
|
||||
if (admin.id === event.userId) continue
|
||||
if (sentEmails.has(admin.email)) continue
|
||||
|
||||
await this.mailService.sendTemplate(admin.email, 'New comment on your photo', TEMPLATES.commentNotification, {
|
||||
userName: commenter.name,
|
||||
content: event.content,
|
||||
photoUrl,
|
||||
replyToUser: undefined,
|
||||
photoId: event.photoId,
|
||||
})
|
||||
sentEmails.add(admin.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
be/apps/core/src/modules/mail/mail.module.ts
Normal file
12
be/apps/core/src/modules/mail/mail.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@afilmory/framework'
|
||||
import { SystemSettingModule } from 'core/modules/configuration/system-setting/system-setting.module'
|
||||
|
||||
import { CommentNotificationListener } from './listeners/comment-notification.listener'
|
||||
import { MailService } from './mail.service'
|
||||
|
||||
@Module({
|
||||
imports: [SystemSettingModule],
|
||||
providers: [MailService, CommentNotificationListener],
|
||||
exports: [MailService],
|
||||
})
|
||||
export class MailModule {}
|
||||
56
be/apps/core/src/modules/mail/mail.service.ts
Normal file
56
be/apps/core/src/modules/mail/mail.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { env } from '@afilmory/env'
|
||||
import { createLogger } from '@afilmory/framework'
|
||||
import ejs from 'ejs'
|
||||
import { Resend } from 'resend'
|
||||
import { injectable } from 'tsyringe'
|
||||
|
||||
import baseTemplate from './templates/base.ejs?raw'
|
||||
import commentNotificationTemplate from './templates/comment-notification.ejs?raw'
|
||||
|
||||
export const TEMPLATES = {
|
||||
commentNotification: commentNotificationTemplate,
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MailService {
|
||||
private readonly logger = createLogger('MailService')
|
||||
private resend: Resend | null = null
|
||||
|
||||
constructor() {
|
||||
if (env.RESEND_API_KEY) {
|
||||
this.resend = new Resend(env.RESEND_API_KEY)
|
||||
} else {
|
||||
this.logger.warn('RESEND_API_KEY is not set. Mail service will be disabled.')
|
||||
}
|
||||
}
|
||||
|
||||
async send(to: string, subject: string, html: string) {
|
||||
if (!this.resend) {
|
||||
this.logger.warn(`Attempted to send email to ${to} but Resend is not configured.`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await this.resend.emails.send({
|
||||
from: env.RESEND_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
})
|
||||
this.logger.info(`Email sent to ${to}, id: ${data.data?.id}`)
|
||||
this.logger.verbose(data)
|
||||
return data
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send email to ${to}`, error)
|
||||
// We don't throw here to prevent blocking the main flow, but we log it.
|
||||
// Or should we throw? For notifications, maybe better to just log.
|
||||
}
|
||||
}
|
||||
|
||||
async sendTemplate(to: string, subject: string, template: string, data: Record<string, any>) {
|
||||
const content = ejs.render(template, data)
|
||||
const html = ejs.render(baseTemplate, { ...data, content, title: subject })
|
||||
|
||||
return this.send(to, subject, html)
|
||||
}
|
||||
}
|
||||
26
be/apps/core/src/modules/mail/templates/base.ejs
Normal file
26
be/apps/core/src/modules/mail/templates/base.ejs
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= title %></title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f9f9f9; }
|
||||
.container { max-width: 600px; margin: 40px auto; padding: 32px; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
|
||||
.header { margin-bottom: 24px; border-bottom: 1px solid #eee; padding-bottom: 16px; }
|
||||
.footer { margin-top: 32px; font-size: 0.85em; color: #888; border-top: 1px solid #eee; padding-top: 16px; text-align: center; }
|
||||
a { color: #000; text-decoration: underline; }
|
||||
h1 { font-size: 20px; margin: 0; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><%= title %></h1>
|
||||
</div>
|
||||
<%- content %>
|
||||
<div class="footer">
|
||||
<p>Sent from Afilmory</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p><strong><%= userName %></strong> <%= replyToUser ? 'replied to you' : 'commented on your photo' %>:</p>
|
||||
<blockquote style="background: #f5f5f5; padding: 12px; border-left: 4px solid #333; margin: 16px 0; white-space: pre-wrap;"><%= content %></blockquote>
|
||||
<p style="margin-top: 24px;">
|
||||
<a href="<%= photoUrl %>" style="display: inline-block; background: #000; color: #fff; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: 500;">View on Site</a>
|
||||
</p>
|
||||
4
be/packages/env/src/index.ts
vendored
4
be/packages/env/src/index.ts
vendored
@@ -28,6 +28,10 @@ export const env = createEnv({
|
||||
CREEM_API_KEY: z.string().min(1),
|
||||
CREEM_WEBHOOK_SECRET: z.string().min(1),
|
||||
|
||||
// Mail
|
||||
RESEND_API_KEY: z.string().min(1).optional(),
|
||||
RESEND_FROM: z.string().min(1).default('AFILMORY <notification@afilmory.art>'),
|
||||
|
||||
DEFAULT_SUPERADMIN_EMAIL: z.email().default('root@local.host'),
|
||||
DEFAULT_SUPERADMIN_USERNAME: z
|
||||
.string()
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"comments.chooseProvider": "Choose sign in method",
|
||||
"comments.empty": "No comments yet. Be the first to comment!",
|
||||
"comments.error": "Failed to load comments",
|
||||
"comments.hint": "Press Enter to send, Shift+Enter for new line",
|
||||
"comments.loadMore": "Load more",
|
||||
"comments.loading": "Loading comments...",
|
||||
"comments.loginRequired": "Please sign in to leave a comment.",
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"comments.chooseProvider": "选择登录方式",
|
||||
"comments.empty": "暂无评论。快来发表第一条评论吧!",
|
||||
"comments.error": "评论加载失败",
|
||||
"comments.hint": "按 Enter 发送,Shift+Enter 换行",
|
||||
"comments.loadMore": "加载更多",
|
||||
"comments.loading": "正在加载评论…",
|
||||
"comments.loginRequired": "登录后才能发表评论。",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './index.css'
|
||||
|
||||
import { clsxm } from '@afilmory/utils'
|
||||
import * as ScrollAreaBase from '@radix-ui/react-scroll-area'
|
||||
import clsx from 'clsx'
|
||||
@@ -72,7 +74,7 @@ Scrollbar.displayName = 'ScrollArea.Scrollbar'
|
||||
const Viewport = ({
|
||||
ref: forwardedRef,
|
||||
className,
|
||||
|
||||
mask = false,
|
||||
focusable = true,
|
||||
...rest
|
||||
}: React.ComponentPropsWithoutRef<typeof ScrollAreaBase.Viewport> & {
|
||||
@@ -89,11 +91,7 @@ const Viewport = ({
|
||||
{...rest}
|
||||
ref={ref}
|
||||
tabIndex={focusable ? -1 : void 0}
|
||||
className={clsxm(
|
||||
'block size-full',
|
||||
|
||||
className,
|
||||
)}
|
||||
className={clsxm('block size-full', mask && 'mask-scroller', className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
44
packages/ui/src/scroll-areas/index.css
Normal file
44
packages/ui/src/scroll-areas/index.css
Normal file
@@ -0,0 +1,44 @@
|
||||
.mask-scroller {
|
||||
mask:
|
||||
linear-gradient(white, transparent) 50% 0 / 100% 0 no-repeat,
|
||||
linear-gradient(white, white) 50% 50% / 100% 100% no-repeat,
|
||||
linear-gradient(transparent, white) 50% 100% / 100% 30px no-repeat;
|
||||
mask-composite: exclude;
|
||||
mask-size:
|
||||
100% calc((var(--scroll-progress-top) / 100) * 30px),
|
||||
100% 100%,
|
||||
100% calc((100 - (100 * (var(--scroll-progress-bottom) / 100))) * 1px);
|
||||
}
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.mask-scroller {
|
||||
mask:
|
||||
linear-gradient(white, transparent) 50% 0 / 100% 0 no-repeat,
|
||||
linear-gradient(white, white) 50% 50% / 100% 100% no-repeat,
|
||||
linear-gradient(transparent, white) 50% 100% / 100% 30px no-repeat;
|
||||
mask-composite: exclude;
|
||||
animation:
|
||||
mask-up both linear,
|
||||
mask-down both linear;
|
||||
animation-timeline: scroll(self);
|
||||
animation-range:
|
||||
0 50px,
|
||||
calc(100% - 50px) 100%;
|
||||
}
|
||||
}
|
||||
@keyframes mask-up {
|
||||
100% {
|
||||
mask-size:
|
||||
100% 30px,
|
||||
100% 100%,
|
||||
100% 30px;
|
||||
}
|
||||
}
|
||||
@keyframes mask-down {
|
||||
100% {
|
||||
mask-size:
|
||||
100% 30px,
|
||||
100% 100%,
|
||||
100% 0;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PhotoManifestItem } from '@afilmory/builder'
|
||||
|
||||
const GENERATOR_NAME = 'Afilmory Feed Generator'
|
||||
const EXIF_NAMESPACE = 'https://afilmory.com/rss/exif'
|
||||
const EXIF_NAMESPACE = 'https://afilmory.art/rss/exif'
|
||||
const PROTOCOL_VERSION = '1.1'
|
||||
const PROTOCOL_ID = 'afilmory-rss-exif'
|
||||
|
||||
|
||||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
@@ -819,7 +819,7 @@ importers:
|
||||
version: 0.5.19(tailwindcss@4.1.17)
|
||||
'@tailwindcss/vite':
|
||||
specifier: 4.1.17
|
||||
version: 4.1.17(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
'@types/node':
|
||||
specifier: 24.10.1
|
||||
version: 24.10.1
|
||||
@@ -831,7 +831,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.7)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
version: 5.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
ast-kit:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
@@ -888,10 +888,10 @@ importers:
|
||||
version: 0.15.4
|
||||
vite-plugin-html:
|
||||
specifier: 3.2.2
|
||||
version: 3.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
version: 3.2.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
vite-plugin-pwa:
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
version: 1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
|
||||
|
||||
be:
|
||||
dependencies:
|
||||
@@ -977,6 +977,9 @@ importers:
|
||||
drizzle-orm:
|
||||
specifier: ^0.44.7
|
||||
version: 0.44.7(@types/pg@8.15.6)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
||||
ejs:
|
||||
specifier: 3.1.10
|
||||
version: 3.1.10
|
||||
hono:
|
||||
specifier: 4.10.7
|
||||
version: 4.10.7
|
||||
@@ -995,6 +998,9 @@ importers:
|
||||
reflect-metadata:
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2
|
||||
resend:
|
||||
specifier: 6.5.2
|
||||
version: 6.5.2
|
||||
satori:
|
||||
specifier: 0.18.3
|
||||
version: 0.18.3
|
||||
@@ -1005,6 +1011,9 @@ importers:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@types/ejs':
|
||||
specifier: 3.1.5
|
||||
version: 3.1.5
|
||||
'@types/mime-types':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
@@ -1608,7 +1617,7 @@ importers:
|
||||
version: 0.16.7(synckit@0.11.11)(typescript@5.9.3)
|
||||
unplugin-dts:
|
||||
specifier: 1.0.0-beta.6
|
||||
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rolldown@1.0.0-beta.51)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
|
||||
vite:
|
||||
specifier: 7.2.4
|
||||
version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
@@ -5659,6 +5668,9 @@ packages:
|
||||
'@splinetool/runtime@0.9.526':
|
||||
resolution: {integrity: sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ==}
|
||||
|
||||
'@stablelib/base64@1.0.1':
|
||||
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
|
||||
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
@@ -6091,6 +6103,9 @@ packages:
|
||||
'@types/deep-eql@4.0.2':
|
||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||
|
||||
'@types/ejs@3.1.5':
|
||||
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
|
||||
@@ -6149,6 +6164,9 @@ packages:
|
||||
'@types/node@20.19.25':
|
||||
resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==}
|
||||
|
||||
'@types/node@22.19.1':
|
||||
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
|
||||
|
||||
'@types/node@24.10.1':
|
||||
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
|
||||
|
||||
@@ -7877,6 +7895,9 @@ packages:
|
||||
es-toolkit@1.42.0:
|
||||
resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
|
||||
|
||||
es6-promise@4.2.8:
|
||||
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
|
||||
|
||||
esast-util-from-estree@2.0.0:
|
||||
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
|
||||
|
||||
@@ -8276,6 +8297,9 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-sha256@1.3.0:
|
||||
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
|
||||
|
||||
fast-shallow-equal@1.0.0:
|
||||
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
|
||||
|
||||
@@ -10410,6 +10434,9 @@ packages:
|
||||
resolution: {integrity: sha512-3jTGGLRzlhu/1ws2zlr4Q+GVMLCQTLFOj8CMX5x44cdZG9FQE07x2mQhaNxaKVPNmIDu0mvJ/cEwtY7Pim7hqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -11094,9 +11121,21 @@ packages:
|
||||
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
||||
engines: {node: '>=0.10.5'}
|
||||
|
||||
requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resend@6.5.2:
|
||||
resolution: {integrity: sha512-Yl83UvS8sYsjgmF8dVbNPzlfpmb3DkLUk3VwsAbkaEFo9UMswpNuPGryHBXGk+Ta4uYMv5HmjVk3j9jmNkcEDg==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
'@react-email/render': '*'
|
||||
peerDependenciesMeta:
|
||||
'@react-email/render':
|
||||
optional: true
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
@@ -11599,6 +11638,9 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
svix@1.76.1:
|
||||
resolution: {integrity: sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==}
|
||||
|
||||
swiper@12.0.3:
|
||||
resolution: {integrity: sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==}
|
||||
engines: {node: '>= 4.7.0'}
|
||||
@@ -12096,6 +12138,9 @@ packages:
|
||||
resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
|
||||
use-callback-ref@1.3.3:
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -12154,6 +12199,10 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
@@ -17356,6 +17405,8 @@ snapshots:
|
||||
on-change: 4.0.2
|
||||
semver-compare: 1.0.0
|
||||
|
||||
'@stablelib/base64@1.0.1': {}
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@stitches/react@1.2.8(react@19.2.0)':
|
||||
@@ -17782,6 +17833,8 @@ snapshots:
|
||||
|
||||
'@types/deep-eql@4.0.2': {}
|
||||
|
||||
'@types/ejs@3.1.5': {}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@@ -17838,6 +17891,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.19.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@24.10.1':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -19750,6 +19807,8 @@ snapshots:
|
||||
|
||||
es-toolkit@1.42.0: {}
|
||||
|
||||
es6-promise@4.2.8: {}
|
||||
|
||||
esast-util-from-estree@2.0.0:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
@@ -20493,6 +20552,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-sha256@1.3.0: {}
|
||||
|
||||
fast-shallow-equal@1.0.0: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
@@ -22965,6 +23026,8 @@ snapshots:
|
||||
filter-obj: 5.1.0
|
||||
split-on-first: 3.0.0
|
||||
|
||||
querystringify@2.2.0: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quickselect@3.0.0: {}
|
||||
@@ -23933,8 +23996,14 @@ snapshots:
|
||||
|
||||
requireindex@1.2.0: {}
|
||||
|
||||
requires-port@1.0.0: {}
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resend@6.5.2:
|
||||
dependencies:
|
||||
svix: 1.76.1
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -24529,6 +24598,15 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svix@1.76.1:
|
||||
dependencies:
|
||||
'@stablelib/base64': 1.0.1
|
||||
'@types/node': 22.19.1
|
||||
es6-promise: 4.2.8
|
||||
fast-sha256: 1.3.0
|
||||
url-parse: 1.5.10
|
||||
uuid: 10.0.0
|
||||
|
||||
swiper@12.0.3: {}
|
||||
|
||||
swr@2.3.6(react@19.2.0):
|
||||
@@ -24945,7 +25023,7 @@ snapshots:
|
||||
magic-string-ast: 1.0.3
|
||||
unplugin: 2.3.10
|
||||
|
||||
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.27.0)(rolldown@1.0.0-beta.51)(rollup@4.53.3)(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.3)
|
||||
'@volar/typescript': 2.4.23
|
||||
@@ -24959,6 +25037,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@microsoft/api-extractor': 7.52.13(@types/node@24.10.1)
|
||||
esbuild: 0.27.0
|
||||
rolldown: 1.0.0-beta.51
|
||||
rollup: 4.53.3
|
||||
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
@@ -25038,6 +25117,11 @@ snapshots:
|
||||
|
||||
url-join@5.0.0: {}
|
||||
|
||||
url-parse@1.5.10:
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
|
||||
use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
@@ -25085,6 +25169,8 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
v8n@1.5.1: {}
|
||||
@@ -25181,7 +25267,7 @@ snapshots:
|
||||
optionator: 0.9.4
|
||||
typescript: 5.9.3
|
||||
|
||||
vite-plugin-html@3.2.2(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
vite-plugin-html@3.2.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -25195,14 +25281,14 @@ snapshots:
|
||||
html-minifier-terser: 6.1.0
|
||||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
|
||||
vite-plugin-pwa@1.1.0(vite@7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
|
||||
vite-plugin-pwa@1.1.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
pretty-bytes: 6.1.1
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.2.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
|
||||
workbox-build: 7.3.0(@types/babel__core@7.20.5)
|
||||
workbox-window: 7.3.0
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:exif="https://afilmory.com/rss/exif">
|
||||
<rss version="2.0" xmlns:exif="https://afilmory.art/rss/exif">
|
||||
<channel>
|
||||
<title><![CDATA[我的风景摄影画廊]]></title>
|
||||
<link>https://example.com</link>
|
||||
|
||||
@@ -45,7 +45,7 @@ const defaultConfig: SiteConfig = {
|
||||
name: 'New Afilmory',
|
||||
title: 'New Afilmory',
|
||||
description: 'A modern photo gallery website.',
|
||||
url: 'https://afilmory.com',
|
||||
url: 'https://afilmory.art',
|
||||
accentColor: '#007bff',
|
||||
author: {
|
||||
name: 'Afilmory',
|
||||
|
||||
Reference in New Issue
Block a user