Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
branches:
- main
- development
workflow_dispatch: # Allows manual triggering
workflow_dispatch: # Allows manual triggering

jobs:
lint:
Expand Down Expand Up @@ -37,9 +37,6 @@ jobs:
with:
node-version: 20
- run: pnpm install
# Copy the example custom-seeds file to satisfy TypeScript during CI
- name: Create custom-seeds.ts from example for typecheck
run: cp apps/web/src/lib/db/seeds/custom-seeds.example.ts apps/web/src/lib/db/seeds/custom-seeds.ts
- run: pnpm check-types

build:
Expand All @@ -56,7 +53,4 @@ jobs:
with:
node-version: 20
- run: pnpm install
# Copy the example custom-seeds file to satisfy TypeScript during CI build
- name: Create custom-seeds.ts from example for build
run: cp apps/web/src/lib/db/seeds/custom-seeds.example.ts apps/web/src/lib/db/seeds/custom-seeds.ts
- run: pnpm build
2 changes: 1 addition & 1 deletion apps/web/scripts/rebuildRenderedHtml.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { markdownService } from "~/lib/services/markdown";
import { logger } from "~/lib/utils/logger";
import { logger } from "@repo/logger";

async function main() {
await markdownService.rebuildAllRenderedHtml();
Expand Down
102 changes: 76 additions & 26 deletions apps/web/src/app/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { eq } from "drizzle-orm";
import { getServerSession } from "next-auth";
import { authOptions } from "~/lib/auth";
import { Suspense } from "react";
import { PageLocationEditor } from "~/components/wiki/PageLocationEditor";
import { renderWikiMarkdownToHtml } from "~/lib/services/markdown";
import { authorizationService } from "~/lib/services/authorization";
import { MovePageWrapper } from "~/components/wiki/MovePageWrapper";

export const dynamic = "auto";
export const revalidate = 300; // 5 minutes
export const fetchCache = "force-cache";

async function getWikiPageByPath(path: string[]) {
export async function getWikiPageByPath(path: string[]) {
// Decode each path segment individually
const decodedPath = path.map((segment) => decodeURIComponent(segment));
const joinedPath = decodedPath.join("/").replace("%20", " ");
Expand All @@ -36,26 +36,41 @@ async function getWikiPageByPath(path: string[]) {
},
});

if (!page) {
return null; // Return null if page not found
}

// Ensure tags are loaded even if we return early due to cached HTML
const tags = page.tags;

// Check if rendered HTML is up-to-date
if (
page?.renderedHtml &&
page?.renderedHtmlUpdatedAt &&
page?.renderedHtmlUpdatedAt > (page?.updatedAt ?? new Date())
page.renderedHtml &&
page.renderedHtmlUpdatedAt &&
page.renderedHtmlUpdatedAt > (page.updatedAt ?? new Date(0)) // Use epoch if no updatedAt
) {
return page;
// Return page with guaranteed tags
return { ...page, tags };
}

// If page is found and has content, pre-render the markdown to HTML with wiki link validation
if (page && page.content) {
// If page is found and has content, render the markdown to HTML
if (page.content) {
const renderedHtml = await renderWikiMarkdownToHtml(
page.content,
page.id,
page.path
);
page.renderedHtml = renderedHtml;
page.renderedHtmlUpdatedAt = new Date();
// Return page with newly rendered HTML and guaranteed tags
return {
...page,
renderedHtml,
renderedHtmlUpdatedAt: new Date(),
tags, // Ensure tags are included here too
};
}

return page;
// If no content, return page with guaranteed tags
return { ...page, tags };
}

type Params = Promise<{ path: string[] }>;
Expand Down Expand Up @@ -95,6 +110,30 @@ export default async function WikiPageView({
notFound();
}

// Format tags for the WikiPage component
const formattedTags =
page.tags?.map((relation) => ({
id: relation.tag.id,
name: relation.tag.name,
})) || [];

// Determine if the page is currently locked
const isLocked = Boolean(
page.lockedBy &&
page.lockExpiresAt &&
new Date(page.lockExpiresAt) > new Date()
);

// Determine if the current user is the lock owner
const isCurrentUserLockOwner = Boolean(
currentUserId && page.lockedBy && page.lockedBy.id === currentUserId
);

// Format the lockedBy data for the header
const formattedLockedBy = page.lockedBy
? { id: page.lockedBy.id, name: page.lockedBy.name || "Unknown" }
: null;

// Check operation modes
const isEditMode = resolvedSearchParams.edit === "true";
const isMoveMode = resolvedSearchParams.move === "true";
Expand Down Expand Up @@ -129,31 +168,42 @@ export default async function WikiPageView({
if (!canMovePage) {
redirect("/");
}
// Instead of trying to render the PageLocationEditor directly,
// use the MainLayout with a client component wrapper
return (
<MainLayout>
<PageLocationEditor
mode="move"
isOpen={true}
onClose={() => {}}
initialPath={page.path.split("/").slice(0, -1).join("/")}
initialName={page.path.split("/").pop() || ""}
<MainLayout
pageMetadata={{
title: page.title,
path: page.path,
id: page.id,
isLocked: isLocked,
lockedBy: formattedLockedBy,
lockExpiresAt: page.lockExpiresAt?.toISOString() || null,
isCurrentUserLockOwner: isCurrentUserLockOwner,
}}
>
<MovePageWrapper
pageId={page.id}
pageTitle={page.title}
pagePath={page.path}
/>
</MainLayout>
);
}

// Format tags for the WikiPage component
const formattedTags =
page.tags?.map((relation) => ({
id: relation.tag.id,
name: relation.tag.name,
})) || [];

// View mode
return (
<MainLayout>
<MainLayout
pageMetadata={{
title: page.title,
path: page.path,
id: page.id,
isLocked: isLocked,
lockedBy: formattedLockedBy,
lockExpiresAt: page.lockExpiresAt?.toISOString() || null,
isCurrentUserLockOwner: isCurrentUserLockOwner,
}}
>
<WikiPage
id={page.id}
title={page.title}
Expand Down
112 changes: 57 additions & 55 deletions apps/web/src/app/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
"use client";

import { useState, useEffect } from "react";
import { Card } from "@repo/ui";
import { useTRPC } from "~/server/client";
import { useQuery } from "@tanstack/react-query";
import type { AppRouter } from "~/server/routers";

interface StatsItem {
title: string;
value: number | string;
icon: React.ReactNode;
}
// Define type for the stats object returned by the API
type SystemStatsQueryProcedure = AppRouter["admin"]["system"]["getStats"];
type SystemStats = Awaited<ReturnType<SystemStatsQueryProcedure>>;

// Define type for the static part
// Define type for the static part, adding a key to link to API data
interface StaticStatData {
title: string;
icon: React.ReactNode;
key: keyof SystemStats; // Key to match the field in SystemStats
}

// Define static parts outside the component with explicit type
// TODO: Make stats clickable and redirect to the respective page

// Define static parts outside the component with explicit type and key
const staticStatsData: StaticStatData[] = [
{
title: "Total Pages",
key: "pageCount",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -37,7 +41,8 @@ const staticStatsData: StaticStatData[] = [
),
},
{
title: "Total Users",
title: "Total Tags",
key: "tagCount",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -50,13 +55,14 @@ const staticStatsData: StaticStatData[] = [
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
d="M12.586 2.586a2 2 0 00-2.828 0L7 5.172V4a1 1 0 10-2 0v4.5a.5.5 0 00.146.354l6.5 6.5a2 2 0 002.828 0l4.5-4.5a2 2 0 000-2.828l-6.914-6.914zM6 8.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z"
/>
</svg>
),
},
{
title: "Total Assets",
key: "assetCount",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -74,8 +80,29 @@ const staticStatsData: StaticStatData[] = [
</svg>
),
},
{
title: "Total Users",
key: "userCount",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
className="text-secondary-400 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
),
},
{
title: "User Groups",
key: "groupCount",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -96,45 +123,20 @@ const staticStatsData: StaticStatData[] = [
];

export default function AdminDashboardPage() {
const [isLoading, setIsLoading] = useState(true);

// Initialize state with static titles/icons and placeholder values
const [stats, setStats] = useState<StatsItem[]>(() =>
staticStatsData.map((item) => ({ ...item, value: "..." }))
);

// Simulate loading data
useEffect(() => {
const timer = setTimeout(() => {
// Create the new state manually, using non-null assertions
const newStats: StatsItem[] = [
{
title: staticStatsData[0]!.title,
icon: staticStatsData[0]!.icon,
value: 42,
},
{
title: staticStatsData[1]!.title,
icon: staticStatsData[1]!.icon,
value: 15,
},
{
title: staticStatsData[2]!.title,
icon: staticStatsData[2]!.icon,
value: 87,
},
{
title: staticStatsData[3]!.title,
icon: staticStatsData[3]!.icon,
value: 5,
},
];
setStats(newStats);
setIsLoading(false);
}, 1000);
// Fetch system stats using tRPC and TanStack Query
const trpc = useTRPC();
const {
data: statsData,
isLoading,
error,
} = useQuery(trpc.admin.system.getStats.queryOptions());

return () => clearTimeout(timer);
}, []); // Keep empty dependency array
// Handle error state
if (error) {
return (
<div>Error loading system stats: {error.message}. Check permissions.</div>
);
}

return (
<div className="space-y-6">
Expand All @@ -143,22 +145,22 @@ export default function AdminDashboardPage() {
<p className="text-text-secondary">Overview of your NextWiki system</p>
</div>

<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => (
<Card key={index} className="p-6">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-5">
{staticStatsData.map((statDef) => (
<Card key={statDef.key} className="p-6">
<div className="flex items-center">
<div className="bg-background-level1 rounded-full p-3">
{stat.icon}
{statDef.icon}
</div>
<div className="ml-4">
<p className="text-text-secondary text-sm font-medium">
{stat.title}
{statDef.title}
</p>
<p className="text-2xl font-semibold">
{isLoading ? (
{isLoading || !statsData ? (
<span className="bg-background-level1 inline-block h-8 w-16 animate-pulse rounded"></span>
) : (
stat.value
statsData[statDef.key]
)}
</p>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/app/admin/groups/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export async function generateMetadata({
};
}

export default async function EditGroupPage({ params }: EditGroupPageProps) {
export default async function EditGroupPage(
propsPromise: Promise<EditGroupPageProps>
) {
const { params } = await propsPromise;
const session = await getServerAuthSession();

// Redirect if not logged in
Expand Down
Loading
Loading