mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-08-18 07:29:40 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 204922570d | |||
| 97d98b82c6 | |||
| 8b81ffa975 |
+41
-41
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.20.4",
|
||||
"version": "3.21.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
@@ -55,40 +55,40 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"@astrojs/mdx": "5.0.0",
|
||||
"@astrojs/node": "10.1.4",
|
||||
"@astrojs/react": "^5.0.7",
|
||||
"@better-auth/oauth-provider": "1.6.11",
|
||||
"@better-auth/sso": "1.6.11",
|
||||
"@astrojs/mdx": "^7.0.3",
|
||||
"@astrojs/node": "^11.0.2",
|
||||
"@astrojs/react": "^6.0.1",
|
||||
"@better-auth/oauth-provider": "1.6.23",
|
||||
"@better-auth/sso": "1.6.23",
|
||||
"@octokit/plugin-throttling": "^11.0.3",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.13",
|
||||
"@radix-ui/react-avatar": "^1.1.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.13",
|
||||
"@radix-ui/react-dialog": "^1.1.16",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
||||
"@radix-ui/react-hover-card": "^1.1.16",
|
||||
"@radix-ui/react-label": "^2.1.9",
|
||||
"@radix-ui/react-popover": "^1.1.16",
|
||||
"@radix-ui/react-progress": "^1.1.9",
|
||||
"@radix-ui/react-radio-group": "^1.4.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.11",
|
||||
"@radix-ui/react-select": "^2.3.0",
|
||||
"@radix-ui/react-separator": "^1.1.9",
|
||||
"@radix-ui/react-slot": "^1.2.5",
|
||||
"@radix-ui/react-switch": "^1.3.0",
|
||||
"@radix-ui/react-tabs": "^1.1.14",
|
||||
"@radix-ui/react-tooltip": "^1.2.9",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@radix-ui/react-accordion": "^1.2.16",
|
||||
"@radix-ui/react-avatar": "^1.2.2",
|
||||
"@radix-ui/react-checkbox": "^1.3.7",
|
||||
"@radix-ui/react-collapsible": "^1.1.16",
|
||||
"@radix-ui/react-dialog": "^1.1.19",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
||||
"@radix-ui/react-hover-card": "^1.1.19",
|
||||
"@radix-ui/react-label": "^2.1.11",
|
||||
"@radix-ui/react-popover": "^1.1.19",
|
||||
"@radix-ui/react-progress": "^1.1.12",
|
||||
"@radix-ui/react-radio-group": "^1.4.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.14",
|
||||
"@radix-ui/react-select": "^2.3.3",
|
||||
"@radix-ui/react-separator": "^1.1.11",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@radix-ui/react-switch": "^1.3.3",
|
||||
"@radix-ui/react-tabs": "^1.1.17",
|
||||
"@radix-ui/react-tooltip": "^1.2.12",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.14.2",
|
||||
"@tanstack/react-virtual": "^3.14.6",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^6.4.6",
|
||||
"astro": "^7.1.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "1.6.11",
|
||||
"better-auth": "1.6.23",
|
||||
"buffer": "^6.0.3",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -96,37 +96,37 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"fuse.js": "^7.4.2",
|
||||
"fuse.js": "^7.5.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"lucide-react": "^0.577.0",
|
||||
"nanoid": "^5.1.11",
|
||||
"lucide-react": "^1.24.0",
|
||||
"nanoid": "^6.0.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-icons": "^5.6.0",
|
||||
"react-icons": "^5.7.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"uuid": "^13.0.2",
|
||||
"typescript": "^7.0.2",
|
||||
"uuid": "^14.0.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.9.3",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"jsdom": "^28.1.0",
|
||||
"tsx": "^4.22.4",
|
||||
"vitest": "^4.1.8"
|
||||
"jsdom": "^29.1.1",
|
||||
"tsx": "^4.23.1",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"packageManager": "bun@1.3.10"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export function NotificationSettings({
|
||||
</Label>
|
||||
<Select
|
||||
value={notificationConfig.provider}
|
||||
onValueChange={(value: "ntfy" | "apprise") =>
|
||||
onValueChange={(value: "ntfy" | "apprise" | "gotify") =>
|
||||
onNotificationChange({ ...notificationConfig, provider: value })
|
||||
}
|
||||
>
|
||||
@@ -103,6 +103,7 @@ export function NotificationSettings({
|
||||
<SelectContent>
|
||||
<SelectItem value="ntfy">Ntfy.sh</SelectItem>
|
||||
<SelectItem value="apprise">Apprise API</SelectItem>
|
||||
<SelectItem value="gotify">Gotify</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -307,6 +308,92 @@ export function NotificationSettings({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gotify configuration */}
|
||||
{notificationConfig.provider === "gotify" && (
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<h3 className="text-sm font-medium">Gotify Settings</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gotify-url" className="text-sm">
|
||||
Server URL <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="gotify-url"
|
||||
type="url"
|
||||
placeholder="https://gotify.example.com"
|
||||
value={notificationConfig.gotify?.url || ""}
|
||||
onChange={(e) =>
|
||||
onNotificationChange({
|
||||
...notificationConfig,
|
||||
gotify: {
|
||||
...notificationConfig.gotify!,
|
||||
url: e.target.value,
|
||||
token: notificationConfig.gotify?.token || "",
|
||||
priority: notificationConfig.gotify?.priority ?? 5,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
URL of your Gotify server
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gotify-token" className="text-sm">
|
||||
Application token <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="gotify-token"
|
||||
type="password"
|
||||
placeholder="A1b2C3d4..."
|
||||
value={notificationConfig.gotify?.token || ""}
|
||||
onChange={(e) =>
|
||||
onNotificationChange({
|
||||
...notificationConfig,
|
||||
gotify: {
|
||||
...notificationConfig.gotify!,
|
||||
url: notificationConfig.gotify?.url || "",
|
||||
token: e.target.value,
|
||||
priority: notificationConfig.gotify?.priority ?? 5,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Create an application in Gotify and paste its token here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gotify-priority" className="text-sm">
|
||||
Default priority (0-10)
|
||||
</Label>
|
||||
<Input
|
||||
id="gotify-priority"
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
value={notificationConfig.gotify?.priority ?? 5}
|
||||
onChange={(e) =>
|
||||
onNotificationChange({
|
||||
...notificationConfig,
|
||||
gotify: {
|
||||
...notificationConfig.gotify!,
|
||||
url: notificationConfig.gotify?.url || "",
|
||||
token: notificationConfig.gotify?.token || "",
|
||||
priority: Math.min(10, Math.max(0, Number(e.target.value) || 0)),
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Error notifications always use priority 8 regardless of this setting
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event toggles */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<h3 className="text-sm font-medium">Notification Events</h3>
|
||||
|
||||
@@ -138,14 +138,21 @@ export const appriseConfigSchema = z.object({
|
||||
tag: z.string().optional(),
|
||||
});
|
||||
|
||||
export const gotifyConfigSchema = z.object({
|
||||
url: z.string().default(""),
|
||||
token: z.string().default(""),
|
||||
priority: z.number().int().min(0).max(10).default(5),
|
||||
});
|
||||
|
||||
export const notificationConfigSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
provider: z.enum(["ntfy", "apprise"]).default("ntfy"),
|
||||
provider: z.enum(["ntfy", "apprise", "gotify"]).default("ntfy"),
|
||||
notifyOnSyncError: z.boolean().default(true),
|
||||
notifyOnSyncSuccess: z.boolean().default(false),
|
||||
notifyOnNewRepo: z.boolean().default(false),
|
||||
ntfy: ntfyConfigSchema.optional(),
|
||||
apprise: appriseConfigSchema.optional(),
|
||||
gotify: gotifyConfigSchema.optional(),
|
||||
});
|
||||
|
||||
export type NotificationConfig = z.infer<typeof notificationConfigSchema>;
|
||||
|
||||
@@ -71,6 +71,32 @@ describe("sendNotification", () => {
|
||||
expect(url).toBe("http://apprise:8000/notify/my-token");
|
||||
});
|
||||
|
||||
test("sends gotify notification when provider is gotify", async () => {
|
||||
const config: NotificationConfig = {
|
||||
enabled: true,
|
||||
provider: "gotify",
|
||||
notifyOnSyncError: true,
|
||||
notifyOnSyncSuccess: true,
|
||||
notifyOnNewRepo: false,
|
||||
gotify: {
|
||||
url: "https://gotify.example.com",
|
||||
token: "my-app-token",
|
||||
priority: 5,
|
||||
},
|
||||
};
|
||||
|
||||
await sendNotification(config, {
|
||||
title: "Test",
|
||||
message: "Test message",
|
||||
type: "sync_success",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
const [url, opts] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe("https://gotify.example.com/message");
|
||||
expect(opts.headers["X-Gotify-Key"]).toBe("my-app-token");
|
||||
});
|
||||
|
||||
test("does not throw when fetch fails", async () => {
|
||||
mockFetch = mock(() => Promise.reject(new Error("Network error")));
|
||||
globalThis.fetch = mockFetch as any;
|
||||
@@ -140,6 +166,29 @@ describe("sendNotification", () => {
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("skips notification when gotify token is missing", async () => {
|
||||
const config: NotificationConfig = {
|
||||
enabled: true,
|
||||
provider: "gotify",
|
||||
notifyOnSyncError: true,
|
||||
notifyOnSyncSuccess: true,
|
||||
notifyOnNewRepo: false,
|
||||
gotify: {
|
||||
url: "https://gotify.example.com",
|
||||
token: "",
|
||||
priority: 5,
|
||||
},
|
||||
};
|
||||
|
||||
await sendNotification(config, {
|
||||
title: "Test",
|
||||
message: "Test message",
|
||||
type: "sync_success",
|
||||
});
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("testNotification", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NotificationConfig } from "@/types/config";
|
||||
import type { NotificationEvent } from "./providers/ntfy";
|
||||
import { sendNtfyNotification } from "./providers/ntfy";
|
||||
import { sendAppriseNotification } from "./providers/apprise";
|
||||
import { sendGotifyNotification } from "./providers/gotify";
|
||||
import { db, configs } from "@/lib/db";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@/lib/utils/encryption";
|
||||
@@ -52,6 +53,12 @@ export async function sendNotification(
|
||||
return;
|
||||
}
|
||||
await sendAppriseNotification(config.apprise, event);
|
||||
} else if (config.provider === "gotify") {
|
||||
if (!config.gotify?.url || !config.gotify?.token) {
|
||||
console.warn("[NotificationService] Gotify URL or token is not configured, skipping notification");
|
||||
return;
|
||||
}
|
||||
await sendGotifyNotification(config.gotify, event);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[NotificationService] Failed to send notification:", error);
|
||||
@@ -83,6 +90,11 @@ export async function testNotification(
|
||||
return { success: false, error: "Apprise URL and token are required" };
|
||||
}
|
||||
await sendAppriseNotification(notificationConfig.apprise, event);
|
||||
} else if (notificationConfig.provider === "gotify") {
|
||||
if (!notificationConfig.gotify?.url || !notificationConfig.gotify?.token) {
|
||||
return { success: false, error: "Gotify URL and token are required" };
|
||||
}
|
||||
await sendGotifyNotification(notificationConfig.gotify, event);
|
||||
} else {
|
||||
return { success: false, error: `Unknown provider: ${notificationConfig.provider}` };
|
||||
}
|
||||
@@ -166,6 +178,12 @@ export async function triggerJobNotification({
|
||||
token: decrypt(decryptedConfig.apprise.token),
|
||||
};
|
||||
}
|
||||
if (decryptedConfig.provider === "gotify" && decryptedConfig.gotify?.token) {
|
||||
decryptedConfig.gotify = {
|
||||
...decryptedConfig.gotify,
|
||||
token: decrypt(decryptedConfig.gotify.token),
|
||||
};
|
||||
}
|
||||
|
||||
// Build event
|
||||
const repoLabel = repositoryName || organizationName || "Unknown";
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, test, expect, beforeEach, mock } from "bun:test";
|
||||
import { sendGotifyNotification } from "./gotify";
|
||||
import type { NotificationEvent } from "./ntfy";
|
||||
import type { GotifyConfig } from "@/types/config";
|
||||
|
||||
describe("sendGotifyNotification", () => {
|
||||
let mockFetch: ReturnType<typeof mock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch = mock(() =>
|
||||
Promise.resolve(new Response("ok", { status: 200 }))
|
||||
);
|
||||
globalThis.fetch = mockFetch as any;
|
||||
});
|
||||
|
||||
const baseConfig: GotifyConfig = {
|
||||
url: "https://gotify.example.com",
|
||||
token: "AbCdEf123456",
|
||||
priority: 5,
|
||||
};
|
||||
|
||||
const baseEvent: NotificationEvent = {
|
||||
title: "Test Notification",
|
||||
message: "This is a test",
|
||||
type: "sync_success",
|
||||
};
|
||||
|
||||
test("constructs correct URL from config", async () => {
|
||||
await sendGotifyNotification(baseConfig, baseEvent);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe("https://gotify.example.com/message");
|
||||
});
|
||||
|
||||
test("strips trailing slash from URL", async () => {
|
||||
await sendGotifyNotification(
|
||||
{ ...baseConfig, url: "https://gotify.example.com/" },
|
||||
baseEvent
|
||||
);
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe("https://gotify.example.com/message");
|
||||
});
|
||||
|
||||
test("sends token via X-Gotify-Key header", async () => {
|
||||
await sendGotifyNotification(baseConfig, baseEvent);
|
||||
|
||||
const [, opts] = mockFetch.mock.calls[0];
|
||||
expect(opts.headers["X-Gotify-Key"]).toBe("AbCdEf123456");
|
||||
});
|
||||
|
||||
test("sends title and message in JSON body", async () => {
|
||||
await sendGotifyNotification(baseConfig, baseEvent);
|
||||
|
||||
const [, opts] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(opts.body);
|
||||
expect(body.title).toBe("Test Notification");
|
||||
expect(body.message).toBe("This is a test");
|
||||
});
|
||||
|
||||
test("uses priority 8 for sync_error events", async () => {
|
||||
const errorEvent: NotificationEvent = {
|
||||
...baseEvent,
|
||||
type: "sync_error",
|
||||
};
|
||||
await sendGotifyNotification(baseConfig, errorEvent);
|
||||
|
||||
const [, opts] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(opts.body);
|
||||
expect(body.priority).toBe(8);
|
||||
});
|
||||
|
||||
test("uses config priority for non-error events", async () => {
|
||||
await sendGotifyNotification(
|
||||
{ ...baseConfig, priority: 2 },
|
||||
baseEvent
|
||||
);
|
||||
|
||||
const [, opts] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(opts.body);
|
||||
expect(body.priority).toBe(2);
|
||||
});
|
||||
|
||||
test("defaults to priority 5 when not configured", async () => {
|
||||
await sendGotifyNotification(
|
||||
{ ...baseConfig, priority: undefined as any },
|
||||
baseEvent
|
||||
);
|
||||
|
||||
const [, opts] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(opts.body);
|
||||
expect(body.priority).toBe(5);
|
||||
});
|
||||
|
||||
test("throws on non-200 response", async () => {
|
||||
mockFetch = mock(() =>
|
||||
Promise.resolve(new Response("unauthorized", { status: 401 }))
|
||||
);
|
||||
globalThis.fetch = mockFetch as any;
|
||||
|
||||
expect(
|
||||
sendGotifyNotification(baseConfig, baseEvent)
|
||||
).rejects.toThrow("Gotify error: 401");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { GotifyConfig } from "@/types/config";
|
||||
import type { NotificationEvent } from "./ntfy";
|
||||
|
||||
export async function sendGotifyNotification(config: GotifyConfig, event: NotificationEvent): Promise<void> {
|
||||
const url = `${config.url.replace(/\/$/, "")}/message`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Gotify-Key": config.token,
|
||||
};
|
||||
const body = JSON.stringify({
|
||||
title: event.title,
|
||||
message: event.message,
|
||||
priority: event.type === "sync_error" ? 8 : (config.priority ?? 5),
|
||||
});
|
||||
const resp = await fetch(url, { method: "POST", body, headers });
|
||||
if (!resp.ok) throw new Error(`Gotify error: ${resp.status} ${await resp.text()}`);
|
||||
}
|
||||
@@ -172,6 +172,13 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
token: encrypt(processedNotificationConfig.apprise.token),
|
||||
};
|
||||
}
|
||||
// Encrypt gotify token if present
|
||||
if (processedNotificationConfig.gotify?.token) {
|
||||
processedNotificationConfig.gotify = {
|
||||
...processedNotificationConfig.gotify,
|
||||
token: encrypt(processedNotificationConfig.gotify.token),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (existingConfig) {
|
||||
@@ -354,6 +361,13 @@ export const GET: APIRoute = async ({ request, locals }) => {
|
||||
notificationConfig.apprise = { ...notificationConfig.apprise, token: "" };
|
||||
}
|
||||
}
|
||||
if (notificationConfig.gotify?.token) {
|
||||
try {
|
||||
notificationConfig.gotify = { ...notificationConfig.gotify, token: decrypt(notificationConfig.gotify.token) };
|
||||
} catch {
|
||||
notificationConfig.gotify = { ...notificationConfig.gotify, token: "" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
|
||||
+8
-1
@@ -119,14 +119,21 @@ export interface AppriseConfig {
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface GotifyConfig {
|
||||
url: string;
|
||||
token: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface NotificationConfig {
|
||||
enabled: boolean;
|
||||
provider: "ntfy" | "apprise";
|
||||
provider: "ntfy" | "apprise" | "gotify";
|
||||
notifyOnSyncError: boolean;
|
||||
notifyOnSyncSuccess: boolean;
|
||||
notifyOnNewRepo: boolean;
|
||||
ntfy?: NtfyConfig;
|
||||
apprise?: AppriseConfig;
|
||||
gotify?: GotifyConfig;
|
||||
}
|
||||
|
||||
export interface Config extends ConfigType {}
|
||||
|
||||
+14
-9
@@ -9,27 +9,32 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^5.0.6",
|
||||
"@astrojs/react": "^5.0.7",
|
||||
"@radix-ui/react-slot": "^1.2.5",
|
||||
"@astrojs/mdx": "^7.0.3",
|
||||
"@astrojs/react": "^6.0.1",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@splinetool/react-spline": "^4.1.0",
|
||||
"@splinetool/runtime": "^1.12.97",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@splinetool/runtime": "^1.12.98",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^6.4.6",
|
||||
"astro": "^7.1.0",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"lucide-react": "^1.24.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.1"
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1"
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1113
-682
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
---
|
||||
import { Github, Book, MessageSquare, Bug } from 'lucide-react';
|
||||
import { Book, MessageSquare, Bug } from 'lucide-react';
|
||||
import { GithubIcon } from './icons/GithubIcon';
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: "Source Code",
|
||||
href: "https://github.com/RayLabsHQ/gitea-mirror",
|
||||
icon: Github
|
||||
icon: GithubIcon
|
||||
},
|
||||
{
|
||||
title: "Documentation",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Github, Star } from 'lucide-react';
|
||||
import { Star } from 'lucide-react';
|
||||
import { GithubIcon } from './icons/GithubIcon';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function GitHubButton() {
|
||||
@@ -44,7 +45,7 @@ export function GitHubButton() {
|
||||
asChild
|
||||
>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer" className="flex items-center">
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
<GithubIcon className="w-4 h-4 mr-2" />
|
||||
<span>Star on GitHub</span>
|
||||
{stars !== null && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
// GitHub brand mark. Replaces lucide-react's `Github` icon, which was removed
|
||||
// along with all brand icons in lucide-react v1.
|
||||
export function GithubIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user