Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
.turbo
.next
.docusaurus
packages/shared-db/migrations
apps/app/src/routeTree.gen.ts
packages/shared-db/migrations
111 changes: 111 additions & 0 deletions apps/api/src/routes/api/processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,117 @@ const StepIdParamSchema = z.object({
stepId: z.string(),
});

// Reprocess a job with the latest feed config
processingRoutes.post(
"/jobs/:jobId/reprocess",
zValidator("param", JobIdParamSchema),
async (c) => {
const { jobId } = c.req.valid("param");
const sp = c.var.sp;

try {
const processingService = sp.getProcessingService();
const newJob = await processingService.reprocessWithLatestConfig(jobId);

return c.json(
ProcessingJobRetryResponseSchema.parse({
statusCode: 200,
success: true,
data: {
job: newJob,
message: "Job reprocessing initiated successfully.",
},
}),
);
} catch (error: unknown) {
sp.getLogger().error(
{ error, jobId },
"Error in processingRoutes.post('/jobs/:jobId/reprocess')",
);

if (error instanceof NotFoundError || error instanceof ServiceError) {
return c.json(
ApiErrorResponseSchema.parse({
statusCode: error.statusCode as ContentfulStatusCode,
success: false,
error: { message: error.message },
}),
error.statusCode as ContentfulStatusCode,
);
}

return c.json(
ApiErrorResponseSchema.parse({
statusCode: 500,
success: false,
error: { message: "Failed to reprocess job" },
}),
500,
);
}
},
);

// Tweak a step's input and reprocess from that point
const TweakStepBodySchema = z.object({
newInput: z.string(),
});

processingRoutes.post(
"/steps/:stepId/tweak",
zValidator("param", StepIdParamSchema),
zValidator("json", TweakStepBodySchema),
async (c) => {
const { stepId } = c.req.valid("param");
const { newInput } = c.req.valid("json");
const sp = c.var.sp;

try {
const processingService = sp.getProcessingService();
const newJob = await processingService.tweakAndReprocessStep(
stepId,
newInput,
);

return c.json(
ProcessingJobRetryResponseSchema.parse({
statusCode: 200,
success: true,
data: {
job: newJob,
message: "Step tweak and reprocess initiated successfully.",
},
}),
);
} catch (error: unknown) {
sp.getLogger().error(
{ error, stepId },
"Error in processingRoutes.post('/steps/:stepId/tweak')",
);

if (error instanceof NotFoundError || error instanceof ServiceError) {
return c.json(
ApiErrorResponseSchema.parse({
statusCode: error.statusCode as ContentfulStatusCode,
success: false,
error: { message: error.message },
}),
error.statusCode as ContentfulStatusCode,
);
}

return c.json(
ApiErrorResponseSchema.parse({
statusCode: 500,
success: false,
error: { message: "Failed to tweak and reprocess step" },
}),
500,
);
}
},
);

// Retry processing from a specific failed step
processingRoutes.post(
"/steps/:stepId/retry",
Expand Down
39 changes: 39 additions & 0 deletions apps/api/src/routes/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FeedService } from "@curatedotfun/core-services";
import {
ApiErrorResponseSchema,
CreateUserRequestSchema,
FeedsWrappedResponseSchema,
UpdateUserRequestSchema,
UserDeletedWrappedResponseSchema,
UserNearAccountIdParamSchema,
Expand Down Expand Up @@ -333,4 +335,41 @@ usersRoutes.get(
},
);

usersRoutes.get(
"/:nearAccountId/feeds",
zValidator("param", UserNearAccountIdParamSchema),
async (c) => {
const { nearAccountId } = c.req.valid("param");
const sp = c.var.sp;

try {
const feedService: FeedService = sp.getFeedService();
const feeds = await feedService.getFeedsByCreator(nearAccountId);

return c.json(
FeedsWrappedResponseSchema.parse({
statusCode: 200,
success: true,
data: feeds.map((feed) => ({
...feed,
config: feed.config,
})),
}),
);
} catch (error) {
c.var.sp
.getLogger()
.error({ error }, `Error fetching feeds for ${nearAccountId}`);
return c.json(
ApiErrorResponseSchema.parse({
statusCode: 500,
success: false,
error: { message: "Failed to fetch feeds" },
}),
500,
);
}
},
);

export { usersRoutes };
4 changes: 2 additions & 2 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"date-fns": "^4.1.0",
"fastintear": "latest",
"immer": "^10.1.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lucide-react": "^0.483.0",
"near-sign-verify": "^0.4.1",
"pinata-web3": "^0.5.4",
Expand All @@ -66,7 +66,7 @@
"@rsbuild/plugin-react": "1.1.0",
"@tanstack/router-devtools": "1.97.23",
"@tanstack/router-plugin": "^1.121.34",
"@types/lodash": "^4.17.18",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.6.0",
Expand Down
30 changes: 12 additions & 18 deletions apps/app/src/components/FilterControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Filter, Search } from "lucide-react";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { SortOrderType, StatusFilterType } from "../lib/api";
import { Button } from "./ui/button";
import { debounce } from "lodash-es";
import { Input } from "./ui/input";
import {
Select,
Expand Down Expand Up @@ -36,7 +37,6 @@ const FilterControls: React.FC<FilterControlsProps> = ({
initialSortOrder || "newest",
);
const [showFiltersDropdown, setShowFiltersDropdown] = useState(false);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);

type TargetSearchSchema =
FileRouteTypes["fileRoutesById"][typeof parentRouteId]["preLoaderRoute"]["validateSearch"]["_output"];
Expand All @@ -48,31 +48,27 @@ const FilterControls: React.FC<FilterControlsProps> = ({
setSortOrder(initialSortOrder || "newest");
}, [initialQ, initialStatus, initialSortOrder]);

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setSearchQuery(newValue);

if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}

debounceTimerRef.current = setTimeout(() => {
const debouncedNavigate = useRef(
debounce((newValue) => {
navigate({
// @ts-expect-error tanstack router types are hard for a dynamic route
search: (prev: TargetSearchSchema) => ({
...prev,
q: newValue || undefined,
// page: 1, // Optional: Reset page on filter change
}),
replace: true,
});
}, 300);
}, 300),
).current;

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setSearchQuery(newValue);
debouncedNavigate(newValue);
};

const handleApplyFiltersClick = () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debouncedNavigate.cancel();
console.log("status", status);
navigate({
// @ts-expect-error tanstack router types are hard for a dynamic route
Expand All @@ -91,9 +87,7 @@ const FilterControls: React.FC<FilterControlsProps> = ({
useEffect(() => {
// Cleanup debounce timer
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debouncedNavigate.cancel();
};
}, []);

Expand Down
22 changes: 22 additions & 0 deletions apps/app/src/components/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { LeaderboardEntry } from "../lib/api";
import { Container } from "./Container";
import { Hero } from "./Hero";
Expand Down Expand Up @@ -33,6 +34,8 @@ export default React.memo(function Leaderboard({
handleTimeDropdownToggle,
handleFeedDropdownClose,
handleTimeDropdownClose,
expandAllRows,
collapseAllRows,
feedDropdownRef,
timeDropdownRef,
table,
Expand Down Expand Up @@ -63,6 +66,25 @@ export default React.memo(function Leaderboard({
timeDropdownRef={timeDropdownRef}
/>

{hasData && (
<div className="flex justify-end gap-2 mb-4">
<button
onClick={expandAllRows}
className="flex items-center gap-1.5 px-3 py-2 text-sm border border-neutral-300 rounded-md bg-white hover:bg-neutral-50 transition-colors text-[#111111]"
>
<ChevronDown className="h-4 w-4" />
Expand All
</button>
<button
onClick={collapseAllRows}
className="flex items-center gap-1.5 px-3 py-2 text-sm border border-neutral-300 rounded-md bg-white hover:bg-neutral-50 transition-colors text-[#111111]"
>
<ChevronUp className="h-4 w-4" />
Collapse All
</button>
</div>
)}

<LeaderboardTable
table={table}
isLoading={isLoading}
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function UserMenu({ className }: UserMenuProps) {
) : (
<ProfileImage size="small" />
)}
<p className="text-sm font-medium leading-6 hidden sm:block">
<p className="text-sm font-medium leading-6">
{getUserDisplayName()}
</p>
<ChevronDown
Expand Down
53 changes: 53 additions & 0 deletions apps/app/src/components/coming-soon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Clock, Sparkles } from "lucide-react";
import { Card } from "./ui/card";
import { Badge } from "./ui/badge";

interface ComingSoonProps {
title: string;
description?: string;
features?: string[];
}

export function ComingSoon({ title, description, features }: ComingSoonProps) {
return (
<Card className="p-6 space-y-6">
<div className="text-center space-y-4">
<div className="flex items-center justify-center space-x-2">
<Sparkles className="h-8 w-8" />
<h2 className="text-2xl font-semibold">{title}</h2>
</div>

<Badge
variant="secondary"
className="flex items-center space-x-1 w-fit mx-auto"
>
<Clock className="h-3 w-3" />
<span>Coming Soon</span>
</Badge>

{description && (
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
{description}
</p>
)}
</div>

{features && features.length > 0 && (
<div className="space-y-3">
<h3 className="text-lg font-medium text-center">What to expect:</h3>
<ul className="space-y-2 max-w-md mx-auto">
{features.map((feature, index) => (
<li
key={index}
className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400"
>
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
)}
</Card>
);
}
Loading