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:
Thomas Camlong
2025-12-26 15:58:20 +01:00
parent 67f0bc5637
commit 16ac249bf8
3 changed files with 93 additions and 136 deletions

View File

@@ -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",
}
}
}

View File

@@ -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",
});
})
},
});
})
}

View File

@@ -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]) {