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
2 changes: 1 addition & 1 deletion dev-share-ui/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground min-h-screen flex flex-col;
}
}
9 changes: 6 additions & 3 deletions dev-share-ui/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/ThemeProvider';
import { Toaster } from 'sonner';
import Navbar from '@/components/Navbar';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
title: 'blotz dev share',
description: 'A place to discover and share the best developer resources with the community',
description:
'A place to discover and share the best developer resources with the community',
};

export default function RootLayout({
Expand All @@ -25,10 +27,11 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
{children}
<Navbar />
<main className="flex flex-col grow">{children}</main>
</ThemeProvider>
<Toaster />
</body>
</html>
);
}
}
13 changes: 13 additions & 0 deletions dev-share-ui/app/result/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SearchBar from '@/components/SearchBar';
import ResourceTable from '@/components/ResourceTable';

export default function ResultPage() {
return (
<div className="min-h-screen bg-background py-10 flex flex-col gap-4 items-center px-20">
<div className="w-full max-w-7xl">
<SearchBar />
</div>
<ResourceTable />
</div>
);
}
67 changes: 21 additions & 46 deletions dev-share-ui/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
"use client";
'use client';

import { useState } from "react";
import Navbar from "@/components/Navbar";
import HeroSection from "@/components/HeroSection";
import { mockResources } from "@/lib/data";
import { Resource, VectorSearchResultDTO } from "@/lib/types";
import ResourceCard from "@/components/ResourceCard";
import FromAISearchResourceCard from "@/components/FromAISearchResourceCard";
import{handleGlobalSearch,searchResources} from "@/services/search-service";
import { useState } from 'react';
import HeroSection from '@/components/HeroSection';
import { mockResources } from '@/lib/data';
import ResourceCard from '@/components/ResourceCard';
import FromAISearchResourceCard from '@/components/FromAISearchResourceCard';
import { handleGlobalSearch, searchResources } from '@/services/search-service';

export default function SearchPage() {
const [resources, setResources] = useState(mockResources);
const [isSearching, setIsSearching] = useState(false);
const [SearchQuery, setSearchQuery] = useState("");
const [isAIFallback, setIsAIFallback] = useState(false);
const [SearchQuery, setSearchQuery] = useState('');
const topRelative = 6;



const handleSearch = async (query: string) => {
setIsSearching(true);
setSearchQuery(query);
setResources([]);

const result = await searchResources(query);
if (result.length === 0) {
await handleGlobalSearch();
setIsAIFallback(true);
} else {
setResources(result);
}
Expand All @@ -39,12 +33,16 @@ export default function SearchPage() {
// 1. Send user action to your API
// 2. Update resource in your database
// 3. Use this data to improve recommendations
setResources(prevResources =>
prevResources.map(resource => {

setResources((prevResources) =>
prevResources.map((resource) => {
if (resource.id === id) {
if (action === 'like') {
return { ...resource, likes: resource.isLiked ? resource.likes - 1 : resource.likes + 1, isLiked: !resource.isLiked };
return {
...resource,
likes: resource.isLiked ? resource.likes - 1 : resource.likes + 1,
isLiked: !resource.isLiked,
};
} else {
return { ...resource, isBookmarked: !resource.isBookmarked };
}
Expand All @@ -53,37 +51,14 @@ export default function SearchPage() {
})
);
};

const handleSearchSuggestion = (suggestion: string) => {
handleSearch(suggestion);
};


return (
<main className="min-h-screen bg-background">
<Navbar />
<div className="bg-background flex grow pb-16 justify-center items-center">
<HeroSection onSearch={handleSearch} isSearching={isSearching} />

<div className="container px-4 py-8 mx-auto max-w-7xl">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<span className="text-lg font-medium text-muted-foreground">
{isAIFallback ? "No direct results found. Showing AI suggestions instead." : `${resources.length} resources found`}</span>
</div>
<a href="#" className="text-primary hover:underline flex items-center gap-1 text-sm font-medium">View All <span aria-hidden="true">→</span></a>
</div>
{ (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{resources.sort((a, b) => b.likes - a.likes).map((resource, idx) =>
resource.isAIGenerated ? (
<FromAISearchResourceCard key={resource.id} resource={resource} onAction={handleResourceAction} />
) : (
<ResourceCard key={resource.id} resource={resource} onAction={handleResourceAction} />
)
)}
</div>
)}
</div>
</main>
</div>
);
}
}
171 changes: 4 additions & 167 deletions dev-share-ui/app/share/page.tsx
Original file line number Diff line number Diff line change
@@ -1,172 +1,9 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Link as LinkIcon, MessageSquare } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { toast } from "sonner";
import Navbar from "@/components/Navbar";
import { submitSharedResource} from "@/services/share-service";
import { pollShareStatus } from "@/services/polling-service";
import { ShareResourceForm } from '@/components/ShareResourceForm';

export default function ShareResourcePage() {
const [url, setUrl] = useState("");
const [comment, setComment] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [urlError, setUrlError] = useState("");
const router = useRouter();

const validateUrl = (url: string) => {
try {
new URL(url);
setUrlError("");
return true;
} catch (e) {
setUrlError("Please enter a valid URL (including http:// or https://)");
return false;
}
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!validateUrl(url)) {
return;
}

setIsSubmitting(true);

// TODO: Integration point for AI processing
// 1. Send URL and prompt to backend
// 2. Backend will:
// - Scrape the URL for content
// - Use AI to generate title, description, and tags
// - Store the processed resource
// Example:
// const resource = await processResourceWithAI({ url, prompt });

// Simulate API call
//set the max processing time
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 6000);

//TODO: Move this to "share" service
try {
const taskId = await submitSharedResource(url, comment);
toast.success("Processing resource...please check status later", { duration: 2500 });

const result = await pollShareStatus(taskId, { maxPolls: 5, interval: 3000 });

if (result === "success") {
toast.success("Resource processing completed!");
} else if (result === "failed") {
toast.error("Resource processing failed.");
} else {
toast.error("Resource processing timed out.");
}

router.push("/");
} catch (err: any) {
if (err.name === "AbortError") {
toast.error("Request timed out after 5 seconds.");
} else {
toast.error("An unexpected error occurred.");
}
}
setIsSubmitting(false);
router.push("/");
};

return (
<main className="min-h-screen bg-background">
<Navbar />
<div className="container px-4 py-8 mx-auto max-w-2xl">
<Card className="animate-in fade-in-50 slide-in-from-bottom-8 duration-300 rounded-2xl shadow-lg bg-white">
<CardHeader>
<CardTitle className="text-2xl">Share a Resource</CardTitle>
<CardDescription>
Share a valuable developer resource. Our AI will analyze it and add relevant details.
</CardDescription>
<hr className="my-4 border-muted" />
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="url">Resource URL <span className="text-destructive">*</span></Label>
<div className="relative">
<LinkIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="url"
type="text"
placeholder="https://example.com/resource"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="pl-10 focus:ring-2 focus:ring-primary/30 hover:border-primary transition-all duration-200"
required
/>
</div>
{urlError && <p className="text-sm text-destructive">{urlError}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="comment" className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
Your Comment
</Label>
<Textarea
id="comment"
placeholder="Share your thoughts, experience, or recommendation about this resource (e.g. I read this docs and I learn a lot... very recommend for beginner to React!)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={3}
required
className="focus:ring-2 focus:ring-primary/30 hover:border-primary transition-all duration-200"
/>
<div className="flex items-center justify-between text-xs text-muted-foreground mt-1">
<span>Let others know why you recommend this resource.</span>
<span>{comment.length}/200</span>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-col sm:flex-row sm:justify-between gap-2 mt-2">
<Button
type="button"
variant="outline"
onClick={() => router.push("/")}
className="w-full sm:w-auto"
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || !url.trim()}
className="w-full sm:w-auto bg-primary text-white font-semibold px-6 py-2 rounded-lg flex items-center justify-center gap-2 shadow-md hover:bg-primary/90 transition-all duration-200"
>
{isSubmitting ? (
<>
<div className="h-4 w-4 mr-2 rounded-full border-2 border-current border-t-transparent animate-spin" />
Submitting...
</>
) : (
<>
Share Resource
</>
)}
</Button>
</CardFooter>
</form>
</Card>
</div>
<main className="min-h-80vh bg-background">
<ShareResourceForm />
</main>
);
}
}
Loading
Loading