diff --git a/app/api/applications/edit/route.ts b/app/api/applications/edit/route.ts new file mode 100644 index 0000000..8e6e514 --- /dev/null +++ b/app/api/applications/edit/route.ts @@ -0,0 +1,40 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; + +interface EditRequest { + id: number; + name: string; + description: string; + serverId: number; + icon: string; + publicURL: string; + localURL: string; +} + +export async function PUT(request: NextRequest) { + try { + const body: EditRequest = await request.json(); + const { id, name, description, serverId, icon, publicURL, localURL } = body; + + const existingApp = await prisma.application.findUnique({ where: { id } }); + if (!existingApp) { + return NextResponse.json({ error: "Server not found" }, { status: 404 }); + } + + const updatedApplication = await prisma.application.update({ + where: { id }, + data: { + serverId, + name, + description, + icon, + publicURL, + localURL + } + }); + + return NextResponse.json({ message: "Application updated", application: updatedApplication }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/applications/search/route.ts b/app/api/applications/search/route.ts new file mode 100644 index 0000000..e9f6ec0 --- /dev/null +++ b/app/api/applications/search/route.ts @@ -0,0 +1,32 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; +import Fuse from "fuse.js"; + +interface SearchRequest { + searchterm: string; +} + +export async function POST(request: NextRequest) { + try { + const body: SearchRequest = await request.json(); + const { searchterm } = body; + + const applications = await prisma.application.findMany({}); + + const fuseOptions = { + keys: ['name', 'description'], + threshold: 0.3, + includeScore: true, + }; + + const fuse = new Fuse(applications, fuseOptions); + + const searchResults = fuse.search(searchterm); + + const results = searchResults.map(({ item }) => item); + + return NextResponse.json({ results }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/servers/search/route.ts b/app/api/servers/search/route.ts new file mode 100644 index 0000000..2797331 --- /dev/null +++ b/app/api/servers/search/route.ts @@ -0,0 +1,32 @@ +import { NextResponse, NextRequest } from "next/server"; +import { prisma } from "@/lib/prisma"; +import Fuse from "fuse.js"; + +interface SearchRequest { + searchterm: string; +} + +export async function POST(request: NextRequest) { + try { + const body: SearchRequest = await request.json(); + const { searchterm } = body; + + const servers = await prisma.server.findMany({}); + + const fuseOptions = { + keys: ['name', 'description', 'cpu', 'gpu', 'ram', 'disk'], + threshold: 0.3, + includeScore: true, + }; + + const fuse = new Fuse(servers, fuseOptions); + + const searchResults = fuse.search(searchterm); + + const results = searchResults.map(({ item }) => item); + + return NextResponse.json({ results }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/dashboard/applications/Applications.tsx b/app/dashboard/applications/Applications.tsx index 5ec7b23..0ac2871 100644 --- a/app/dashboard/applications/Applications.tsx +++ b/app/dashboard/applications/Applications.tsx @@ -16,7 +16,16 @@ import { SidebarTrigger, } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; -import { Plus, Link, Home, Trash2, LayoutGrid, List } from "lucide-react"; +import { + Plus, + Link, + Home, + Trash2, + LayoutGrid, + List, + Pencil, + Zap, +} from "lucide-react"; import { Card, CardContent, @@ -89,6 +98,15 @@ export default function Dashboard() { const [publicURL, setPublicURL] = useState(""); const [localURL, setLocalURL] = useState(""); const [serverId, setServerId] = useState(null); + + const [editName, setEditName] = useState(""); + const [editDescription, setEditDescription] = useState(""); + const [editIcon, setEditIcon] = useState(""); + const [editPublicURL, setEditPublicURL] = useState(""); + const [editLocalURL, setEditLocalURL] = useState(""); + const [editId, setEditId] = useState(null); + const [editServerId, setEditServerId] = useState(null); + const [currentPage, setCurrentPage] = useState(1); const [maxPage, setMaxPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(5); @@ -97,9 +115,12 @@ export default function Dashboard() { const [isGridLayout, setIsGridLayout] = useState(false); const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [isSearching, setIsSearching] = useState(false); + useEffect(() => { - const savedLayout = Cookies.get('layoutPreference-app'); - const layout_bool = savedLayout === 'grid'; + const savedLayout = Cookies.get("layoutPreference-app"); + const layout_bool = savedLayout === "grid"; setIsGridLayout(layout_bool); setItemsPerPage(layout_bool ? 15 : 5); }, []); @@ -107,35 +128,35 @@ export default function Dashboard() { const toggleLayout = () => { const newLayout = !isGridLayout; setIsGridLayout(newLayout); - Cookies.set('layoutPreference-app', newLayout ? 'grid' : 'standard', { + Cookies.set("layoutPreference-app", newLayout ? "grid" : "standard", { expires: 365, - path: '/', - sameSite: 'strict' + path: "/", + sameSite: "strict", }); setItemsPerPage(newLayout ? 15 : 5); }; const add = async () => { try { - await axios.post('/api/applications/add', { - name, - description, - icon, - publicURL, + await axios.post("/api/applications/add", { + name, + description, + icon, + publicURL, localURL, - serverId + serverId, }); getApplications(); } catch (error: any) { - console.log(error.response?.data); + console.log(error.response?.data); } - } + }; const getApplications = async () => { try { setLoading(true); const response = await axios.post( - '/api/applications/get', + "/api/applications/get", { page: currentPage, ITEMS_PER_PAGE: itemsPerPage } ); setApplications(response.data.applications); @@ -145,22 +166,88 @@ export default function Dashboard() { } catch (error: any) { console.log(error.response?.data); } - } + }; useEffect(() => { getApplications(); }, [currentPage, itemsPerPage]); - const handlePrevious = () => setCurrentPage(prev => Math.max(1, prev - 1)); - const handleNext = () => setCurrentPage(prev => Math.min(maxPage, prev + 1)); + const handlePrevious = () => setCurrentPage((prev) => Math.max(1, prev - 1)); + const handleNext = () => + setCurrentPage((prev) => Math.min(maxPage, prev + 1)); const deleteApplication = async (id: number) => { try { - await axios.post('/api/applications/delete', { id }); + await axios.post("/api/applications/delete", { id }); getApplications(); } catch (error: any) { console.log(error.response?.data); } + }; + + const openEditDialog = (app: Application) => { + setEditId(app.id); + setEditServerId(app.serverId); + setEditName(app.name); + setEditDescription(app.description || ""); + setEditIcon(app.icon || ""); + setEditLocalURL(app.localURL || ""); + setEditPublicURL(app.publicURL || ""); + }; + + const edit = async () => { + if (!editId) return; + + try { + await axios.put("/api/applications/edit", { + id: editId, + serverId: editServerId, + name: editName, + description: editDescription, + icon: editIcon, + publicURL: editPublicURL, + localURL: editLocalURL, + }); + getApplications(); + setEditId(null); + } catch (error: any) { + console.log(error.response.data); + } + }; + + const searchApplications = async () => { + try { + setIsSearching(true); + const response = await axios.post<{ results: Application[] }>( + "/api/applications/search", + { searchterm: searchTerm } + ); + setApplications(response.data.results); + setIsSearching(false); + } catch (error: any) { + console.error("Search error:", error.response?.data); + setIsSearching(false); + } + }; + + useEffect(() => { + const delayDebounce = setTimeout(() => { + if (searchTerm.trim() === "") { + getApplications(); + } else { + searchApplications(); + } + }, 300); + + return () => clearTimeout(delayDebounce); + }, [searchTerm]); + + const generateIconURL = async () => { + setIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + name.toLowerCase() + ".png") + } + + const generateEditIconURL = async () => { + setEditIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + editName.toLowerCase() + ".png") } return ( @@ -192,16 +279,24 @@ export default function Dashboard() {
Your Applications
- {servers.length === 0 ? ( -

You must first add a server.

+

+ You must first add a server. +

) : ( @@ -214,151 +309,367 @@ export default function Dashboard() { Add an application
-
- - setName(e.target.value)}/> -
-
- - -
-
- -