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
106 changes: 106 additions & 0 deletions app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use client";

import { Title } from "@radix-ui/react-dialog";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useParams } from "next/navigation";
import { parseAsBoolean, useQueryState } from "nuqs";
import { useState } from "react";
import EmptyState from "@/components/core/empty-state";
import { Panel } from "@/components/core/panel";
import PageSection from "@/components/core/section";
import PostForm from "@/components/form/post";
import PageTitle from "@/components/layout/page-title";
import PostsList from "@/components/project/posts/posts-list";
import { buttonVariants } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useTRPC } from "@/trpc/client";

export default function Posts() {
const { projectId, tenant } = useParams();
const [create, setCreate] = useQueryState(
"create",
parseAsBoolean.withDefault(false),
);
const [activeTab, setActiveTab] = useState("published");

const trpc = useTRPC();

const { data: project } = useQuery(
trpc.projects.getProjectById.queryOptions({
id: +projectId!,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate projectId before using non-null assertions.

The pattern +projectId! is used repeatedly (lines 31, 37, 44, 74, 86) to coerce and assert the projectId parameter. This bypasses TypeScript's safety checks and could lead to runtime errors if projectId is undefined or not a valid numeric string.

Consider validating the parameter early in the component:

 export default function Posts() {
 	const { projectId, tenant } = useParams();
+	
+	if (!projectId || Array.isArray(projectId)) {
+		throw new Error("Invalid project ID");
+	}
+	
+	const numericProjectId = Number(projectId);
+	if (Number.isNaN(numericProjectId)) {
+		throw new Error("Project ID must be a number");
+	}
+	
 	const [create, setCreate] = useQueryState(
 		"create",
 		parseAsBoolean.withDefault(false),
 	);

Then use numericProjectId throughout instead of +projectId!.

Also applies to: 37-37, 44-44, 74-74, 86-86

🤖 Prompt for AI Agents
In app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx around lines
31-86, the code uses the non-null assertion/coercion pattern `+projectId!` in
multiple places which bypasses TypeScript safety; validate and normalize
projectId once at the top (e.g., check that projectId exists, parse it to a
number with Number() or parseInt, verify it is not NaN and positive) and assign
to a local const like numericProjectId; if validation fails, return an
appropriate fallback (throw, return notFound(), or render an error) so runtime
errors are avoided, then replace all `+projectId!` occurrences with the
validated numericProjectId.

}),
);

const { data: publishedPosts = [] } = useQuery({
...trpc.posts.list.queryOptions({
projectId: +projectId!,
}),
enabled: activeTab === "published",
});

const { data: myDrafts = [] } = useQuery({
...trpc.posts.myDrafts.queryOptions({
projectId: +projectId!,
}),
enabled: activeTab === "drafts",
});

return (
<>
<PageTitle
title="Posts"
actions={
project?.canEdit ? (
<Link
href={`/${tenant}/projects/${projectId}/posts?create=true`}
className={buttonVariants()}
>
New
</Link>
) : undefined
}
/>

<PageSection transparent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="mb-4">
<TabsTrigger value="published">Published</TabsTrigger>
<TabsTrigger value="drafts">My Drafts</TabsTrigger>
</TabsList>

<TabsContent value="published">
{publishedPosts.length ? (
<PostsList posts={publishedPosts} projectId={+projectId!} />
) : (
<EmptyState
show={!publishedPosts.length}
label="post"
createLink={`/${tenant}/projects/${projectId}/posts?create=true`}
/>
)}
</TabsContent>

<TabsContent value="drafts">
{myDrafts.length ? (
<PostsList posts={myDrafts} projectId={+projectId!} />
) : (
<div className="text-center text-muted-foreground py-8">
No draft posts
</div>
)}
</TabsContent>
</Tabs>
</PageSection>

{project?.canEdit && (
<Panel open={create} setOpen={setCreate}>
<Title>
<PageTitle title="New Post" compact />
</Title>
<PostForm />
</Panel>
)}
</>
);
}
97 changes: 65 additions & 32 deletions bun.lock

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions components/core/search-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
FileText,
Filter,
FolderOpen,
MessagesSquare,
RefreshCw,
Search,
X,
Expand All @@ -36,7 +37,7 @@ interface SearchResult {
id: string;
title: string;
description?: string;
type: "project" | "task" | "tasklist" | "event";
type: "project" | "task" | "tasklist" | "event" | "post";
status?: string;
projectName?: string;
url: string;
Expand All @@ -56,6 +57,8 @@ const getTypeIcon = (type: string) => {
return <FileText className="h-4 w-4" />;
case "event":
return <Calendar className="h-4 w-4" />;
case "post":
return <MessagesSquare className="h-4 w-4" />;
default:
return <Search className="h-4 w-4" />;
}
Expand All @@ -71,6 +74,8 @@ const getTypeLabel = (type: string) => {
return "Task List";
case "event":
return "Event";
case "post":
return "Post";
default:
return type;
}
Expand Down Expand Up @@ -105,7 +110,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {

const [query, setQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<
"project" | "task" | "tasklist" | "event" | undefined
"project" | "task" | "tasklist" | "event" | "post" | undefined
>();
const [projectFilter, setProjectFilter] = useState<number | undefined>();
const [statusFilter, setStatusFilter] = useState<string | undefined>();
Expand Down Expand Up @@ -146,7 +151,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
try {
const result = await indexAllMutation.mutateAsync();
toast.success("Content reindexed successfully!", {
description: `Indexed ${result.indexed.projects} projects, ${result.indexed.taskLists} task lists, ${result.indexed.tasks} tasks, and ${result.indexed.events} events.`,
description: `Indexed ${result.indexed.projects} projects, ${result.indexed.taskLists} task lists, ${result.indexed.tasks} tasks, ${result.indexed.events} events, and ${result.indexed.posts} posts.`,
});
} catch (err) {
console.error("Error reindexing content:", err);
Expand Down Expand Up @@ -185,7 +190,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {

const groupedResults = groupBy<
SearchResult,
"project" | "task" | "tasklist" | "event"
"project" | "task" | "tasklist" | "event" | "post"
>(searchResults, (item) => item.type);

// Reset search when sheet closes
Expand All @@ -203,7 +208,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
<div>
<h2 className="text-lg font-semibold">Search</h2>
<p className="text-sm text-muted-foreground">
Search across all your projects, tasks, events, and more
Search across all your projects, tasks, events, posts, and more
</p>
</div>
<Button
Expand All @@ -222,7 +227,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search projects, tasks, events..."
placeholder="Search projects, tasks, events, posts..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-10 pr-4 h-10"
Expand Down Expand Up @@ -312,6 +317,10 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
<Calendar className="mr-2 h-4 w-4" />
Events
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTypeFilter("post")}>
<MessagesSquare className="mr-2 h-4 w-4" />
Posts
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

Expand Down Expand Up @@ -402,7 +411,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
</h3>
<p className="text-muted-foreground max-w-md">
Type in the search box above to find projects, tasks,
events, and more across your workspace.
events, posts, and more across your workspace.
</p>
</div>
</div>
Expand Down Expand Up @@ -444,7 +453,7 @@ export function SearchSheet({ open, onOpenChange }: SearchSheetProps) {
{searchResults.length !== 1 ? "s" : ""} for "{debouncedQuery}"
</div>

{["project", "tasklist", "task", "event"].map((type) => {
{["project", "tasklist", "task", "event", "post"].map((type) => {
const results =
groupedResults[type as keyof typeof groupedResults];
if (!results || results.length === 0) return null;
Expand Down
Loading