mirror of
https://github.com/homarr-labs/dashboard-icons.git
synced 2026-01-13 00:27:20 +08:00
style: apply code formatting to actions, hooks, and api files
Apply consistent code formatting across multiple files: - Remove semicolons for consistency with project style - Improve code readability - Files: github actions, submission hooks, and api utilities
This commit is contained in:
@@ -1,52 +1,48 @@
|
||||
"use server";
|
||||
"use server"
|
||||
|
||||
import PocketBase from "pocketbase";
|
||||
import PocketBase from "pocketbase"
|
||||
|
||||
const GITHUB_OWNER = "homarr-labs";
|
||||
const GITHUB_REPO = "dashboard-icons";
|
||||
const WORKFLOW_FILE = "add-icon.yml";
|
||||
const GITHUB_OWNER = "homarr-labs"
|
||||
const GITHUB_REPO = "dashboard-icons"
|
||||
const WORKFLOW_FILE = "add-icon.yml"
|
||||
|
||||
interface TriggerWorkflowResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
workflowUrl?: string;
|
||||
success: boolean
|
||||
error?: string
|
||||
workflowUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the provided auth token belongs to an admin user
|
||||
* The token is passed from the client since auth is stored in localStorage
|
||||
*/
|
||||
async function verifyAdmin(
|
||||
authToken: string,
|
||||
): Promise<{ isAdmin: boolean; error?: string }> {
|
||||
async function verifyAdmin(authToken: string): Promise<{ isAdmin: boolean; error?: string }> {
|
||||
if (!authToken) {
|
||||
return { isAdmin: false, error: "Not authenticated" };
|
||||
return { isAdmin: false, error: "Not authenticated" }
|
||||
}
|
||||
|
||||
try {
|
||||
const pb = new PocketBase(
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090",
|
||||
);
|
||||
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090")
|
||||
|
||||
// Validate the token by refreshing auth
|
||||
// This will fail if the token is invalid/expired
|
||||
pb.authStore.save(authToken, null);
|
||||
pb.authStore.save(authToken, null)
|
||||
|
||||
const authData = await pb.collection("users").authRefresh();
|
||||
const authData = await pb.collection("users").authRefresh()
|
||||
|
||||
if (!authData?.record) {
|
||||
return { isAdmin: false, error: "Invalid authentication" };
|
||||
return { isAdmin: false, error: "Invalid authentication" }
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
if (!authData.record.admin) {
|
||||
return { isAdmin: false, error: "User is not an admin" };
|
||||
return { isAdmin: false, error: "User is not an admin" }
|
||||
}
|
||||
|
||||
return { isAdmin: true };
|
||||
return { isAdmin: true }
|
||||
} catch (error) {
|
||||
console.error("Error verifying admin:", error);
|
||||
return { isAdmin: false, error: "Failed to verify admin status" };
|
||||
console.error("Error verifying admin:", error)
|
||||
return { isAdmin: false, error: "Failed to verify admin status" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,21 +53,17 @@ async function verifyAdmin(
|
||||
* @param submissionId - The ID of the submission to add
|
||||
* @param dryRun - If true, skip actual writes (for testing)
|
||||
*/
|
||||
export async function triggerAddIconWorkflow(
|
||||
authToken: string,
|
||||
submissionId: string,
|
||||
dryRun = false,
|
||||
): Promise<TriggerWorkflowResult> {
|
||||
export async function triggerAddIconWorkflow(authToken: string, submissionId: string, dryRun = false): Promise<TriggerWorkflowResult> {
|
||||
// Verify admin status using the provided token
|
||||
const { isAdmin, error: authError } = await verifyAdmin(authToken);
|
||||
const { isAdmin, error: authError } = await verifyAdmin(authToken)
|
||||
if (!isAdmin) {
|
||||
return { success: false, error: authError || "Unauthorized" };
|
||||
return { success: false, error: authError || "Unauthorized" }
|
||||
}
|
||||
|
||||
// Check for GitHub token
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const githubToken = process.env.GITHUB_TOKEN
|
||||
if (!githubToken) {
|
||||
return { success: false, error: "GitHub token not configured" };
|
||||
return { success: false, error: "GitHub token not configured" }
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -93,31 +85,30 @@ export async function triggerAddIconWorkflow(
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("GitHub API error:", response.status, errorText);
|
||||
const errorText = await response.text()
|
||||
console.error("GitHub API error:", response.status, errorText)
|
||||
return {
|
||||
success: false,
|
||||
error: `GitHub API error: ${response.status} - ${errorText}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// The dispatch endpoint returns 204 No Content on success
|
||||
// Construct a URL to the workflow runs page
|
||||
const workflowUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}`;
|
||||
const workflowUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}`
|
||||
|
||||
return {
|
||||
success: true,
|
||||
workflowUrl,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error triggering workflow:", error);
|
||||
console.error("Error triggering workflow:", error)
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Failed to trigger workflow",
|
||||
};
|
||||
error: error instanceof Error ? error.message : "Failed to trigger workflow",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { triggerAddIconWorkflow } from "@/app/actions/github";
|
||||
import { revalidateAllSubmissions } from "@/app/actions/submissions";
|
||||
import { getAllIcons } from "@/lib/api";
|
||||
import { pb, type Submission } from "@/lib/pb";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { triggerAddIconWorkflow } from "@/app/actions/github"
|
||||
import { revalidateAllSubmissions } from "@/app/actions/submissions"
|
||||
import { getAllIcons } from "@/lib/api"
|
||||
import { pb, type Submission } from "@/lib/pb"
|
||||
|
||||
// Query key factory
|
||||
export const submissionKeys = {
|
||||
all: ["submissions"] as const,
|
||||
lists: () => [...submissionKeys.all, "list"] as const,
|
||||
list: (filters?: Record<string, any>) =>
|
||||
[...submissionKeys.lists(), filters] as const,
|
||||
};
|
||||
list: (filters?: Record<string, any>) => [...submissionKeys.lists(), filters] as const,
|
||||
}
|
||||
|
||||
// Fetch all submissions
|
||||
export function useSubmissions() {
|
||||
return useQuery({
|
||||
queryKey: submissionKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const records = await pb
|
||||
.collection("submissions")
|
||||
.getFullList<Submission>({
|
||||
sort: "-updated",
|
||||
expand: "created_by,approved_by",
|
||||
requestKey: null,
|
||||
});
|
||||
const records = await pb.collection("submissions").getFullList<Submission>({
|
||||
sort: "-updated",
|
||||
expand: "created_by,approved_by",
|
||||
requestKey: null,
|
||||
})
|
||||
|
||||
if (records.length > 0) {
|
||||
}
|
||||
|
||||
return records;
|
||||
return records
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Approve submission mutation
|
||||
export function useApproveSubmission() {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (submissionId: string) => {
|
||||
@@ -49,45 +46,36 @@ export function useApproveSubmission() {
|
||||
{
|
||||
requestKey: null,
|
||||
},
|
||||
);
|
||||
)
|
||||
},
|
||||
onSuccess: async (_data) => {
|
||||
// Invalidate and refetch submissions
|
||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
||||
|
||||
// Revalidate Next.js cache for community pages
|
||||
await revalidateAllSubmissions();
|
||||
await revalidateAllSubmissions()
|
||||
|
||||
toast.success("Submission approved", {
|
||||
description: "The submission has been approved successfully",
|
||||
});
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Error approving submission:", error);
|
||||
if (
|
||||
!error.message?.includes("autocancelled") &&
|
||||
!error.name?.includes("AbortError")
|
||||
) {
|
||||
console.error("Error approving submission:", error)
|
||||
if (!error.message?.includes("autocancelled") && !error.name?.includes("AbortError")) {
|
||||
toast.error("Failed to approve submission", {
|
||||
description: error.message || "An error occurred",
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Reject submission mutation
|
||||
export function useRejectSubmission() {
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
submissionId,
|
||||
adminComment,
|
||||
}: {
|
||||
submissionId: string;
|
||||
adminComment?: string;
|
||||
}) => {
|
||||
mutationFn: async ({ submissionId, adminComment }: { submissionId: string; adminComment?: string }) => {
|
||||
return await pb.collection("submissions").update(
|
||||
submissionId,
|
||||
{
|
||||
@@ -98,31 +86,28 @@ export function useRejectSubmission() {
|
||||
{
|
||||
requestKey: null,
|
||||
},
|
||||
);
|
||||
)
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// Invalidate and refetch submissions
|
||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: submissionKeys.lists() })
|
||||
|
||||
// Revalidate Next.js cache for community pages
|
||||
await revalidateAllSubmissions();
|
||||
await revalidateAllSubmissions()
|
||||
|
||||
toast.success("Submission rejected", {
|
||||
description: "The submission has been rejected",
|
||||
});
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Error rejecting submission:", error);
|
||||
if (
|
||||
!error.message?.includes("autocancelled") &&
|
||||
!error.name?.includes("AbortError")
|
||||
) {
|
||||
console.error("Error rejecting submission:", error)
|
||||
if (!error.message?.includes("autocancelled") && !error.name?.includes("AbortError")) {
|
||||
toast.error("Failed to reject submission", {
|
||||
description: error.message || "An error occurred",
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch existing icon names for the combobox + the metadata.json file
|
||||
@@ -134,28 +119,22 @@ export function useExistingIconNames() {
|
||||
fields: "name",
|
||||
sort: "name",
|
||||
requestKey: null,
|
||||
});
|
||||
})
|
||||
|
||||
const metadata = await getAllIcons();
|
||||
const metadataNames = Object.keys(metadata);
|
||||
const metadata = await getAllIcons()
|
||||
const metadataNames = Object.keys(metadata)
|
||||
|
||||
const uniqueRecordsNames = Array.from(
|
||||
new Set(records.map((r) => r.name)),
|
||||
);
|
||||
const uniqueMetadataNames = Array.from(
|
||||
new Set(metadataNames.map((n) => n)),
|
||||
);
|
||||
const uniqueNames = Array.from(
|
||||
new Set(uniqueRecordsNames.concat(uniqueMetadataNames)),
|
||||
);
|
||||
const uniqueRecordsNames = Array.from(new Set(records.map((r) => r.name)))
|
||||
const uniqueMetadataNames = Array.from(new Set(metadataNames.map((n) => n)))
|
||||
const uniqueNames = Array.from(new Set(uniqueRecordsNames.concat(uniqueMetadataNames)))
|
||||
return uniqueNames.map((name) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
}));
|
||||
}))
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Check authentication status
|
||||
@@ -163,67 +142,57 @@ export function useAuth() {
|
||||
return useQuery({
|
||||
queryKey: ["auth"],
|
||||
queryFn: async () => {
|
||||
const isValid = pb.authStore.isValid;
|
||||
const userId = pb.authStore.record?.id;
|
||||
const isValid = pb.authStore.isValid
|
||||
const userId = pb.authStore.record?.id
|
||||
|
||||
if (!isValid || !userId) {
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
isAdmin: false,
|
||||
userId: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch the full user record to get the admin status
|
||||
const user = await pb.collection("users").getOne(userId, {
|
||||
requestKey: null,
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
isAuthenticated: true,
|
||||
isAdmin: user?.admin === true,
|
||||
userId: userId,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
console.error("Error fetching user:", error)
|
||||
return {
|
||||
isAuthenticated: isValid,
|
||||
isAdmin: false,
|
||||
userId: userId || "",
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
retry: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Trigger GitHub workflow to add icon to collection
|
||||
export function useTriggerWorkflow() {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
submissionId,
|
||||
dryRun = false,
|
||||
}: {
|
||||
submissionId: string;
|
||||
dryRun?: boolean;
|
||||
}) => {
|
||||
mutationFn: async ({ submissionId, dryRun = false }: { submissionId: string; dryRun?: boolean }) => {
|
||||
// Get the auth token from the client-side PocketBase instance
|
||||
const authToken = pb.authStore.token;
|
||||
const authToken = pb.authStore.token
|
||||
if (!authToken) {
|
||||
throw new Error("Not authenticated");
|
||||
throw new Error("Not authenticated")
|
||||
}
|
||||
|
||||
const result = await triggerAddIconWorkflow(
|
||||
authToken,
|
||||
submissionId,
|
||||
dryRun,
|
||||
);
|
||||
const result = await triggerAddIconWorkflow(authToken, submissionId, dryRun)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Failed to trigger workflow");
|
||||
throw new Error(result.error || "Failed to trigger workflow")
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success("GitHub workflow triggered", {
|
||||
@@ -234,13 +203,13 @@ export function useTriggerWorkflow() {
|
||||
onClick: () => window.open(data.workflowUrl, "_blank"),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
})
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error("Error triggering workflow:", error);
|
||||
console.error("Error triggering workflow:", error)
|
||||
toast.error("Failed to trigger workflow", {
|
||||
description: error.message || "An error occurred",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,10 +149,7 @@ function buildInternalAuthorData(author: { id: string | number; name?: string; l
|
||||
* This prevents hitting GitHub API rate limits by caching author data
|
||||
* across multiple page builds and requests.
|
||||
*/
|
||||
export async function getAuthorData(
|
||||
authorId: number | string,
|
||||
authorMeta?: { name?: string; login?: string },
|
||||
): Promise<AuthorData> {
|
||||
export async function getAuthorData(authorId: number | string, authorMeta?: { name?: string; login?: string }): Promise<AuthorData> {
|
||||
const cacheKey = String(authorId)
|
||||
|
||||
if (authorDataCache[cacheKey]) {
|
||||
|
||||
Reference in New Issue
Block a user