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
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 };
5 changes: 3 additions & 2 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@tanstack/react-query": "^5.81.2",
"@tanstack/react-router": "^1.121.34",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/zod-form-adapter": "^0.42.1",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
Expand All @@ -42,7 +43,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 +67,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