Compare commits
16 Commits
v0.8.5
...
4d149fd927
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d149fd927 | |||
| 5eef9f2467 | |||
| 3fc68adcd2 | |||
| 7018e2eda5 | |||
| 54364d7a88 | |||
| e65daea8dd | |||
| 23e8ac702e | |||
| d777ee9211 | |||
| 01982964bb | |||
| f3b4fa553b | |||
| e164d51574 | |||
| e66a332270 | |||
| 48d25b668d | |||
| 6eb3dfd8f9 | |||
| ca980fce09 | |||
| 8f2fd89847 |
@@ -23,6 +23,7 @@
|
|||||||
"openid-client": "6.8.4",
|
"openid-client": "6.8.4",
|
||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"pino-http": "11.0.0",
|
"pino-http": "11.0.0",
|
||||||
|
"yaml": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { type Request, type Response, type NextFunction } from "express";
|
|||||||
|
|
||||||
const TIER_FEATURES: Record<string, string[]> = {
|
const TIER_FEATURES: Record<string, string[]> = {
|
||||||
free: ["browse", "rate", "search"],
|
free: ["browse", "rate", "search"],
|
||||||
premium: ["browse", "rate", "search", "similar-tools", "costs", "redundancy", "analytics-advanced", "trash", "compare", "watchlist"],
|
premium: ["browse", "rate", "search", "similar-tools", "costs", "redundancy", "analytics-advanced", "trash", "compare", "watchlist", "tool-import"],
|
||||||
enterprise: ["browse", "rate", "search", "similar-tools", "costs", "redundancy", "analytics-advanced", "trash", "compare", "watchlist", "sso", "audit-export", "api-access"],
|
enterprise: ["browse", "rate", "search", "similar-tools", "costs", "redundancy", "analytics-advanced", "trash", "compare", "watchlist", "tool-import", "sso", "audit-export", "api-access"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getEntitlements(tier: string | undefined, role: string | undefined): string[] {
|
export function getEntitlements(tier: string | undefined, role: string | undefined): string[] {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Router, type IRouter } from "express";
|
import { Router, type IRouter } from "express";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { parse as parseYaml } from "yaml";
|
||||||
import { db, toolsTable, ratingsTable, toolCostsTable, toolRelationsTable } from "@workspace/db";
|
import { db, toolsTable, ratingsTable, toolCostsTable, toolRelationsTable } from "@workspace/db";
|
||||||
import { requireAdmin } from "../middleware/auth";
|
import { requireAdmin } from "../middleware/auth";
|
||||||
|
import { requireFeature } from "../middleware/feature";
|
||||||
import { writeAuditLog } from "../lib/audit";
|
import { writeAuditLog } from "../lib/audit";
|
||||||
|
import { CreateToolBody } from "@workspace/api-zod";
|
||||||
|
|
||||||
const EvaluateBody = z.object({
|
const EvaluateBody = z.object({
|
||||||
toolId: z.coerce.number().int().positive(),
|
toolId: z.coerce.number().int().positive(),
|
||||||
@@ -219,4 +222,184 @@ router.post("/admin/redundancy/evaluate", requireAdmin, async (req, res): Promis
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ToolImportBody = z.object({
|
||||||
|
format: z.enum(["auto", "csv", "json", "yaml"]).default("auto"),
|
||||||
|
delimiter: z.enum(["auto", "comma", "semicolon", "tab"]).default("auto"),
|
||||||
|
data: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const TOOL_FIELDS = ["name", "description", "category", "websiteUrl", "iconUrl", "features", "tags"] as const;
|
||||||
|
|
||||||
|
function splitList(value: unknown): string[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((v) => String(v).trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value
|
||||||
|
.split(/[|;,]/)
|
||||||
|
.map((v) => v.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTool(raw: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const key of TOOL_FIELDS) {
|
||||||
|
if (key in raw) out[key] = raw[key];
|
||||||
|
}
|
||||||
|
if ("features" in out) out.features = splitList(out.features);
|
||||||
|
if ("tags" in out) out.tags = splitList(out.tags);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsv(data: string, delimiter: string): Record<string, unknown>[] {
|
||||||
|
let sep = ",";
|
||||||
|
if (delimiter === "semicolon") sep = ";";
|
||||||
|
else if (delimiter === "tab") sep = "\t";
|
||||||
|
else if (delimiter === "auto") {
|
||||||
|
const firstLine = data.split(/\r?\n/)[0] ?? "";
|
||||||
|
const candidates = [",", ";", "\t"];
|
||||||
|
const counts = candidates.map((c) => (firstLine.match(new RegExp(`\\${c === "\t" ? "\\t" : c}`, "g")) ?? []).length);
|
||||||
|
const max = Math.max(...counts);
|
||||||
|
if (max > 0) sep = candidates[counts.indexOf(max)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = data.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
||||||
|
if (lines.length === 0) return [];
|
||||||
|
const header = parseCsvLine(lines[0], sep);
|
||||||
|
const rows: Record<string, unknown>[] = [];
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cells = parseCsvLine(lines[i], sep);
|
||||||
|
const row: Record<string, unknown> = {};
|
||||||
|
header.forEach((h, idx) => {
|
||||||
|
const key = h.trim();
|
||||||
|
if (key) row[key] = cells[idx] ?? "";
|
||||||
|
});
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsvLine(line: string, sep: string): string[] {
|
||||||
|
const cells: string[] = [];
|
||||||
|
let cur = "";
|
||||||
|
let inQuotes = false;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const ch = line[i];
|
||||||
|
if (inQuotes) {
|
||||||
|
if (ch === '"') {
|
||||||
|
if (line[i + 1] === '"') {
|
||||||
|
cur += '"';
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inQuotes = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cur += ch;
|
||||||
|
}
|
||||||
|
} else if (ch === '"') {
|
||||||
|
inQuotes = true;
|
||||||
|
} else if (ch === sep) {
|
||||||
|
cells.push(cur);
|
||||||
|
cur = "";
|
||||||
|
} else {
|
||||||
|
cur += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells.push(cur);
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/admin/tools/import",
|
||||||
|
requireAdmin,
|
||||||
|
requireFeature("tool-import"),
|
||||||
|
async (req, res): Promise<void> => {
|
||||||
|
const parsed = ToolImportBody.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ error: parsed.error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { format, delimiter, data } = parsed.data;
|
||||||
|
|
||||||
|
let rawRows: Record<string, unknown>[] = [];
|
||||||
|
let effectiveFormat = format;
|
||||||
|
if (format === "auto") {
|
||||||
|
const trimmed = data.trimStart();
|
||||||
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
||||||
|
effectiveFormat = "json";
|
||||||
|
} else if (trimmed.includes(":") && !trimmed.startsWith("\"")) {
|
||||||
|
effectiveFormat = "yaml";
|
||||||
|
} else {
|
||||||
|
effectiveFormat = "csv";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (effectiveFormat === "json") {
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
if (Array.isArray(json)) {
|
||||||
|
rawRows = json.filter((r) => r && typeof r === "object") as Record<string, unknown>[];
|
||||||
|
} else if (json && typeof json === "object") {
|
||||||
|
rawRows = [json as Record<string, unknown>];
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: "JSON data must be an object or an array of objects" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (effectiveFormat === "yaml") {
|
||||||
|
const yaml = parseYaml(data);
|
||||||
|
if (Array.isArray(yaml)) {
|
||||||
|
rawRows = yaml.filter((r) => r && typeof r === "object") as Record<string, unknown>[];
|
||||||
|
} else if (yaml && typeof yaml === "object") {
|
||||||
|
rawRows = [yaml as Record<string, unknown>];
|
||||||
|
} else {
|
||||||
|
res.status(400).json({ error: "YAML data must be an object or a list of objects" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rawRows = parseCsv(data, delimiter);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: `Failed to parse ${effectiveFormat} data: ${(err as Error).message}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: { row: number; error: string }[] = [];
|
||||||
|
const validRows: { name: string; description: string; category: string; websiteUrl?: string; iconUrl?: string; features: string[]; tags: string[]; createdBy: string }[] = [];
|
||||||
|
const user = req.session.user!;
|
||||||
|
const createdBy = user.preferred_username || user.name || user.sub;
|
||||||
|
|
||||||
|
rawRows.forEach((raw, idx) => {
|
||||||
|
const normalized = normalizeTool(raw);
|
||||||
|
const check = CreateToolBody.safeParse(normalized);
|
||||||
|
if (!check.success) {
|
||||||
|
errors.push({ row: idx + 1, error: check.error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validRows.push({
|
||||||
|
name: check.data.name,
|
||||||
|
description: check.data.description,
|
||||||
|
category: check.data.category,
|
||||||
|
websiteUrl: check.data.websiteUrl,
|
||||||
|
iconUrl: check.data.iconUrl,
|
||||||
|
features: check.data.features ?? [],
|
||||||
|
tags: check.data.tags ?? [],
|
||||||
|
createdBy,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let imported = 0;
|
||||||
|
if (validRows.length > 0) {
|
||||||
|
const inserted = await db.insert(toolsTable).values(validRows).returning({ id: toolsTable.id, name: toolsTable.name, category: toolsTable.category });
|
||||||
|
imported = inserted.length;
|
||||||
|
for (const tool of inserted) {
|
||||||
|
await writeAuditLog(req, "tool", tool.id, "create", { name: tool.name, category: tool.category, source: "import" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ imported, total: rawRows.length, errors });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -268,8 +268,8 @@ router.get("/compare", requireFeature("compare"), async (req, res): Promise<void
|
|||||||
res.status(400).json({ error: "Provide at least one ids value, e.g. ?ids=1,2,3" });
|
res.status(400).json({ error: "Provide at least one ids value, e.g. ?ids=1,2,3" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ids.length > 8) {
|
if (ids.length > 9) {
|
||||||
res.status(400).json({ error: "Maximum of 8 tools can be compared" });
|
res.status(400).json({ error: "Maximum of 9 tools can be compared" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tools = await db.select().from(toolsTable).where(and(isNull(toolsTable.deletedAt), inArray(toolsTable.id, ids)));
|
const tools = await db.select().from(toolsTable).where(and(isNull(toolsTable.deletedAt), inArray(toolsTable.id, ids)));
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
@@ -24,12 +23,13 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
import { Search, Trash2, ExternalLink, Pencil, Star } from "lucide-react";
|
import { ToolImportDialog } from "@/components/tool-import-dialog";
|
||||||
|
import { Search, Trash2, ExternalLink, Pencil, Star, Upload } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
export function AdminToolsTab() {
|
export function AdminToolsTab() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isAdmin, isLoading: authLoading } = useAuth();
|
const { isAdmin, hasFeature, isLoading: authLoading } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ export function AdminToolsTab() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||||
const [confirmTrash, setConfirmTrash] = useState(false);
|
const [confirmTrash, setConfirmTrash] = useState(false);
|
||||||
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setTimeout(() => setSearch(searchInput), 300);
|
const t = setTimeout(() => setSearch(searchInput), 300);
|
||||||
@@ -106,15 +107,22 @@ export function AdminToolsTab() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4 flex-wrap">
|
<CardHeader className="flex flex-row items-center justify-between gap-4 flex-wrap">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle>{t("adminTools.allTools")}</CardTitle>
|
||||||
{t("adminTools.allTools")}
|
|
||||||
<GuideHelp guide="administration" label={t("adminTools.allTools")} />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{selectedIds.length > 0 ? t("adminTools.selected", { count: selectedIds.length }) : t("adminTools.toolCount", { count: allTools.length })}
|
{selectedIds.length > 0 ? t("adminTools.selected", { count: selectedIds.length }) : t("adminTools.toolCount", { count: allTools.length })}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{hasFeature("tool-import") && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setImportOpen(true)}
|
||||||
|
data-testid="button-tool-import"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" /> {t("adminTools.importButton")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="w-4 h-4 absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
<Search className="w-4 h-4 absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -126,7 +134,8 @@ export function AdminToolsTab() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="outline"
|
||||||
|
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
disabled={selectedIds.length === 0 || trash.isPending}
|
disabled={selectedIds.length === 0 || trash.isPending}
|
||||||
onClick={() => setConfirmTrash(true)}
|
onClick={() => setConfirmTrash(true)}
|
||||||
>
|
>
|
||||||
@@ -233,6 +242,15 @@ export function AdminToolsTab() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
<ToolImportDialog
|
||||||
|
open={importOpen}
|
||||||
|
onOpenChange={setImportOpen}
|
||||||
|
onImported={() => {
|
||||||
|
toast({ title: t("adminTools.toastImportDone") });
|
||||||
|
invalidate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function CompareBar({
|
|||||||
if (tools.length === 0) return null;
|
if (tools.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-[min(96vw,42rem)]">
|
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 w-[min(96vw,42rem)]">
|
||||||
<div className="rounded-xl border bg-popover/95 backdrop-blur shadow-lg p-3">
|
<div className="neon-glow rounded-xl border bg-popover/95 backdrop-blur shadow-lg p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Scale className="w-4 h-4 text-muted-foreground shrink-0" />
|
<Scale className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||||
<div className="flex items-center gap-1.5 flex-1 min-w-0 overflow-x-auto">
|
<div className="flex items-center gap-1.5 flex-1 min-w-0 overflow-x-auto">
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { Link } from "wouter";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
export function FieldHelp({
|
|
||||||
schema,
|
|
||||||
field,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
schema: string;
|
|
||||||
field: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const label = children ?? field;
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Link
|
|
||||||
href={`/docs/reference/schemas/${schema}#${field}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
aria-label={`Help: ${label}`}
|
|
||||||
data-testid={`help-${schema}-${field}`}
|
|
||||||
className="inline-flex shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
<HelpCircle className="h-3.5 w-3.5" />
|
|
||||||
</Link>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{label} — Details in der Dokumentation</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BookOpen } from "lucide-react";
|
import { HelpCircle } from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export function GuideHelp({
|
export function GuideHelp({
|
||||||
@@ -9,21 +11,24 @@ export function GuideHelp({
|
|||||||
guide: string;
|
guide: string;
|
||||||
label: string;
|
label: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
<Button asChild variant="outline" size="sm" className="gap-1.5 text-muted-foreground">
|
||||||
<Link
|
<Link
|
||||||
href={`/docs/handbook/${guide}`}
|
href={`/docs/handbook/${guide}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
aria-label={`Help: ${label}`}
|
aria-label={`${t("common.help")}: ${label}`}
|
||||||
data-testid={`guide-${guide}`}
|
data-testid={`guide-${guide}`}
|
||||||
className="inline-flex shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
>
|
||||||
<BookOpen className="h-3.5 w-3.5" />
|
<HelpCircle className="h-4 w-4" />
|
||||||
|
{t("common.help")}
|
||||||
</Link>
|
</Link>
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{label} — Anleitung in der Dokumentation</TooltipContent>
|
<TooltipContent>{label} — {t("common.guideTooltip")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { LayoutDashboard, Wrench, BarChart3, FileText, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Search, Trash2, HelpCircle } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
||||||
@@ -39,12 +39,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
||||||
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
||||||
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
||||||
{ href: "/docs", label: t("nav.docs"), icon: FileText },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminLinks = [
|
const adminLinks = [
|
||||||
{ href: "/admin", label: t("nav.admin"), icon: ShieldCheck },
|
{ href: "/admin", label: t("nav.admin"), icon: ShieldCheck },
|
||||||
{ href: "/admin/redundancy", label: t("nav.redundancy"), icon: AlertTriangle },
|
{ href: "/admin/redundancy", label: t("nav.redundancy"), icon: AlertTriangle },
|
||||||
|
{ href: "/trash", label: t("nav.trash"), icon: Trash2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
function isActive(href: string) {
|
function isActive(href: string) {
|
||||||
@@ -109,7 +109,18 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<SidebarFooter className="gap-2">
|
<SidebarFooter className="gap-2">
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
<div className="border-t pt-2 flex items-center justify-between text-[11px] text-muted-foreground px-2 group-data-[collapsible=icon]:justify-center">
|
<div className="border-t pt-2 flex items-center justify-between text-[11px] text-muted-foreground px-2 group-data-[collapsible=icon]:justify-center">
|
||||||
<span className="font-medium group-data-[collapsible=icon]:hidden">toolr</span>
|
<div className="flex items-center gap-1.5 font-medium group-data-[collapsible=icon]:hidden">
|
||||||
|
<span>toolr</span>
|
||||||
|
<a
|
||||||
|
href="/docs"
|
||||||
|
title={t("nav.docs")}
|
||||||
|
aria-label={t("nav.docs")}
|
||||||
|
data-testid="button-help"
|
||||||
|
className="text-muted-foreground hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{version?.commitSha ? (
|
{version?.commitSha ? (
|
||||||
<a
|
<a
|
||||||
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${version.commitSha}`}
|
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${version.commitSha}`}
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export function MiniBars({
|
|||||||
<span className="text-[10px] uppercase tracking-wide text-muted-foreground w-[3.5rem] shrink-0">
|
<span className="text-[10px] uppercase tracking-wide text-muted-foreground w-[3.5rem] shrink-0">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
<div className="flex-1 h-1.5 rounded-full bg-foreground/5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full bg-primary/70"
|
className="neon-bar h-full rounded-full"
|
||||||
style={{ width: `${pct(value)}%` }}
|
style={{ width: `${pct(value)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,17 +67,17 @@ export function MiniBarStack({
|
|||||||
const a = num(usability);
|
const a = num(usability);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("h-1.5 w-16 rounded-full bg-muted overflow-hidden flex", className)}
|
className={cn("h-1.5 w-16 rounded-full bg-foreground/5 overflow-hidden flex", className)}
|
||||||
title={`Usefulness ${u != null ? u.toFixed(1) : "N/A"} / Usability ${
|
title={`Usefulness ${u != null ? u.toFixed(1) : "N/A"} / Usability ${
|
||||||
a != null ? a.toFixed(1) : "N/A"
|
a != null ? a.toFixed(1) : "N/A"
|
||||||
} (of 5)`}
|
} (of 5)`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary/80"
|
className="neon-bar h-full"
|
||||||
style={{ width: `${pct(usefulness)}%` }}
|
style={{ width: `${pct(usefulness)}%` }}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary/40"
|
className="neon-bar h-full opacity-60"
|
||||||
style={{ width: `${pct(usability)}%` }}
|
style={{ width: `${pct(usability)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function RatingStars({ value, max = 5, size = "md", interactive = false,
|
|||||||
data-testid={`star-${starValue}`}
|
data-testid={`star-${starValue}`}
|
||||||
>
|
>
|
||||||
<Star
|
<Star
|
||||||
className={`${sizeClasses[size]} ${isFilled ? "fill-primary text-primary" : "text-muted-foreground/30"} transition-colors`}
|
className={`${sizeClasses[size]} ${isFilled ? "fill-primary text-primary [filter:drop-shadow(0_0_4px_hsl(var(--primary)/0.45))]" : "text-muted-foreground/30"} transition-colors group-hover:brightness-110 ${interactive ? "hover:[filter:drop-shadow(0_0_6px_hsl(var(--glow-1)/0.6))]" : ""}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function ToolCard({
|
|||||||
watchlist?: { watched: boolean; onToggle: () => void };
|
watchlist?: { watched: boolean; onToggle: () => void };
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="hover-elevate transition-all flex flex-col h-full cursor-pointer hover:border-primary/50">
|
<Card className="hover-elevate transition-all flex flex-col h-full cursor-pointer hover:border-primary/50 hover:neon-glow-primary">
|
||||||
<Link href={`/tools/${tool.id}`} className="flex flex-col h-full p-0 m-0">
|
<Link href={`/tools/${tool.id}`} className="flex flex-col h-full p-0 m-0">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start gap-3 mb-2">
|
<div className="flex items-start gap-3 mb-2">
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useImportTools, type ToolImportResponse } from "@workspace/api-client-react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
|
import { Upload, FileUp, CheckCircle2, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
type Format = "auto" | "csv" | "json" | "yaml";
|
||||||
|
type Delimiter = "auto" | "comma" | "semicolon" | "tab";
|
||||||
|
|
||||||
|
export function ToolImportDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onImported,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onImported: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const importTools = useImportTools();
|
||||||
|
const [format, setFormat] = useState<Format>("auto");
|
||||||
|
const [delimiter, setDelimiter] = useState<Delimiter>("auto");
|
||||||
|
const [data, setData] = useState("");
|
||||||
|
const [result, setResult] = useState<ToolImportResponse | null>(null);
|
||||||
|
|
||||||
|
function handleFile(file: File) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setData(String(reader.result ?? ""));
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImport() {
|
||||||
|
importTools.mutate(
|
||||||
|
{ data: { format, delimiter, data } },
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setResult(res);
|
||||||
|
if (res.imported > 0) onImported();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose(open: boolean) {
|
||||||
|
if (!importTools.isPending) {
|
||||||
|
onOpenChange(open);
|
||||||
|
if (!open) {
|
||||||
|
setData("");
|
||||||
|
setResult(null);
|
||||||
|
setFormat("auto");
|
||||||
|
setDelimiter("auto");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSubmit = data.trim().length > 0 && !importTools.isPending;
|
||||||
|
|
||||||
|
const placeholder =
|
||||||
|
format === "csv"
|
||||||
|
? t("adminTools.importTextareaPlaceholderCsv")
|
||||||
|
: format === "json"
|
||||||
|
? t("adminTools.importTextareaPlaceholderJson")
|
||||||
|
: format === "yaml"
|
||||||
|
? t("adminTools.importTextareaPlaceholderYaml")
|
||||||
|
: t("adminTools.importTextareaPlaceholderAuto");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
{t("adminTools.importTitle")}
|
||||||
|
</DialogTitle>
|
||||||
|
<GuideHelp guide="administration" label={t("adminTools.importTitle")} />
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<p className="text-sm text-muted-foreground">{t("adminTools.importSub")}</p>
|
||||||
|
|
||||||
|
<Tabs value={format} onValueChange={(v) => setFormat(v as Format)}>
|
||||||
|
<TabsList className="w-full grid grid-cols-4">
|
||||||
|
<TabsTrigger value="auto">{t("adminTools.importFormatAuto")}</TabsTrigger>
|
||||||
|
<TabsTrigger value="csv">CSV</TabsTrigger>
|
||||||
|
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||||
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="auto" className="text-xs text-muted-foreground pt-2">
|
||||||
|
{t("adminTools.importFormatAutoSub")}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="csv" className="pt-2 space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{t("adminTools.importDelimiter")}</Label>
|
||||||
|
<Select value={delimiter} onValueChange={(v) => setDelimiter(v as Delimiter)}>
|
||||||
|
<SelectTrigger className="w-52">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">{t("adminTools.importDelimiterAuto")}</SelectItem>
|
||||||
|
<SelectItem value="comma">{t("adminTools.importDelimiterComma")}</SelectItem>
|
||||||
|
<SelectItem value="semicolon">{t("adminTools.importDelimiterSemicolon")}</SelectItem>
|
||||||
|
<SelectItem value="tab">{t("adminTools.importDelimiterTab")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("adminTools.importCsvSub")}</p>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="json" className="pt-2 space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">{t("adminTools.importJsonSub")}</p>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="yaml" className="pt-2 space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">{t("adminTools.importYamlSub")}</p>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button type="button" variant="outline" size="sm" className="gap-2" onClick={() => document.getElementById("tool-import-file")?.click()}>
|
||||||
|
<FileUp className="w-4 h-4" />
|
||||||
|
{t("adminTools.importFile")}
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
id="tool-import-file"
|
||||||
|
type="file"
|
||||||
|
accept=".csv,.json,.yaml,.yml,.txt,text/plain,application/json"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
if (f) handleFile(f);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
value={data}
|
||||||
|
onChange={(e) => setData(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="min-h-[220px] font-mono text-sm"
|
||||||
|
data-testid="input-tool-import-data"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{importTools.isError && (
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive">
|
||||||
|
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
|
<span>{importTools.error?.data?.error ?? importTools.error?.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-sm">
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-primary shrink-0" />
|
||||||
|
<span>{t("adminTools.importDone", { count: result.imported, total: result.total })}</span>
|
||||||
|
</div>
|
||||||
|
{result.errors.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium flex items-center gap-1.5">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-500" />
|
||||||
|
{t("adminTools.importErrors", { count: result.errors.length })}
|
||||||
|
</p>
|
||||||
|
<div className="max-h-40 overflow-y-auto rounded-md border bg-muted/40 divide-y divide-border">
|
||||||
|
{result.errors.map((err, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2 px-3 py-1.5 text-xs">
|
||||||
|
<span className="font-mono text-muted-foreground shrink-0">
|
||||||
|
{t("adminTools.importErrorRow", { row: err.row })}:
|
||||||
|
</span>
|
||||||
|
<span className="text-foreground break-words">{err.error}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => handleClose(false)} disabled={importTools.isPending}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleImport} disabled={!canSubmit}>
|
||||||
|
{importTools.isPending ? t("adminTools.importing") : t("adminTools.importButton")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,14 +12,14 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
// @replit: no hover, and add primary border
|
// @replit: no hover, and add primary border
|
||||||
"bg-primary text-primary-foreground border border-primary-border",
|
"bg-primary text-primary-foreground border border-primary-border neon-glow-primary hover:brightness-105 focus-visible:brightness-105 transition-[box-shadow,filter]",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
|
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border shadow-[0_0_12px_-2px_hsl(var(--destructive)/0.5)] hover:brightness-105 transition-[box-shadow,filter]",
|
||||||
outline:
|
outline:
|
||||||
// @replit Shows the background color of whatever card / sidebar / accent background it is inside of.
|
// @replit Shows the background color of whatever card / sidebar / accent background it is inside of.
|
||||||
// Inherits the current text color. Uses shadow-xs. no shadow on active
|
// Inherits the current text color. Uses shadow-xs. no shadow on active
|
||||||
// No hover state
|
// No hover state
|
||||||
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
|
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none hover:neon-glow focus-visible:neon-glow transition-shadow",
|
||||||
secondary:
|
secondary:
|
||||||
// @replit border, no hover, no shadow, secondary border.
|
// @replit border, no hover, no shadow, secondary border.
|
||||||
"border bg-secondary text-secondary-foreground border border-secondary-border ",
|
"border bg-secondary text-secondary-foreground border border-secondary-border ",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-[0_0_0_1px_hsl(var(--ring)/0.2)] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ const Progress = React.forwardRef<
|
|||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
"relative h-2 w-full overflow-hidden rounded-full border border-border/60 bg-foreground/5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
className="neon-bar h-full w-full flex-1 transition-all"
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:w-8! group-data-[collapsible=icon]:h-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[active=true]:neon-glow-primary data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:w-8! group-data-[collapsible=icon]:h-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -530,7 +530,7 @@ function SidebarMenuButton({
|
|||||||
data-sidebar="menu-button"
|
data-sidebar="menu-button"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
className={cn("neon-sidebar-active", sidebarMenuButtonVariants({ variant, size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function Skeleton({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
className={cn("loading-gradient rounded-md border border-border/60", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
|||||||
<Loader2Icon
|
<Loader2Icon
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
className={cn("size-4 animate-spin", className)}
|
className={cn(
|
||||||
|
"size-4 animate-spin text-primary [filter:drop-shadow(0_0_6px_hsl(var(--glow-1)/0.55))]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-[0_0_0_1px_hsl(var(--ring)/0.2)] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -164,11 +164,9 @@ export function UserMenu() {
|
|||||||
|
|
||||||
<Dialog open={pwOpen} onOpenChange={setPwOpen}>
|
<Dialog open={pwOpen} onOpenChange={setPwOpen}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader className="flex flex-row items-start justify-between gap-4 space-y-0">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle>{t("auth.changePassword")}</DialogTitle>
|
||||||
{t("auth.changePassword")}
|
|
||||||
<GuideHelp guide="konto" label={t("auth.changePassword")} />
|
<GuideHelp guide="konto" label={t("auth.changePassword")} />
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!user?.isLocal ? (
|
{!user?.isLocal ? (
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"viewDetails": "Details ansehen",
|
"viewDetails": "Details ansehen",
|
||||||
"guideTooltip": "Anleitung in der Dokumentation",
|
"guideTooltip": "Anleitung in der Dokumentation",
|
||||||
|
"help": "Hilfe",
|
||||||
"viewMode": "Ansichtsmodus",
|
"viewMode": "Ansichtsmodus",
|
||||||
"viewGrid": "Raster",
|
"viewGrid": "Raster",
|
||||||
"viewTable": "Tabelle",
|
"viewTable": "Tabelle",
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"welcome": "Willkommen bei toolr",
|
"welcome": "Willkommen bei toolr",
|
||||||
"tagline": "Die Community-Plattform, auf der Ingenieure die Tools, die sie täglich nutzen, ehrlich bewerten.",
|
"tagline": "Bewerte dein Tool.",
|
||||||
"totalTools": "Tools gesamt",
|
"totalTools": "Tools gesamt",
|
||||||
"totalRatings": "Bewertungen gesamt",
|
"totalRatings": "Bewertungen gesamt",
|
||||||
"avgRating": "Ø Bewertung",
|
"avgRating": "Ø Bewertung",
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
"compareCount": "Vergleichen ({{count}})",
|
"compareCount": "Vergleichen ({{count}})",
|
||||||
"addToCompare": "Zum Vergleich hinzufügen",
|
"addToCompare": "Zum Vergleich hinzufügen",
|
||||||
"removeFromCompare": "Aus dem Vergleich entfernen",
|
"removeFromCompare": "Aus dem Vergleich entfernen",
|
||||||
|
"compareLimitReached": "Maximal 9 Tools können verglichen werden.",
|
||||||
"comparePremiumTitle": "Vergleichen ist ein Premium-Feature",
|
"comparePremiumTitle": "Vergleichen ist ein Premium-Feature",
|
||||||
"comparePremiumSub": "Das Vergleichen von Tools nebeneinander ist für Premium- und Enterprise-Nutzer verfügbar. Erweitere deinen Plan, um es freizuschalten.",
|
"comparePremiumSub": "Das Vergleichen von Tools nebeneinander ist für Premium- und Enterprise-Nutzer verfügbar. Erweitere deinen Plan, um es freizuschalten.",
|
||||||
"notNow": "Später",
|
"notNow": "Später",
|
||||||
@@ -188,7 +190,7 @@
|
|||||||
"nameOptional": "Name (optional)",
|
"nameOptional": "Name (optional)",
|
||||||
"nameLabel": "Name",
|
"nameLabel": "Name",
|
||||||
"anonymousPlaceholder": "Anonym",
|
"anonymousPlaceholder": "Anonym",
|
||||||
"anonymousEngineer": "Anonymer Ingenieur",
|
"anonymousEngineer": "Anonymer Benutzer",
|
||||||
"beFirstToReview": "Sei der Erste, der seine Gedanken zu diesem Tool teilt.",
|
"beFirstToReview": "Sei der Erste, der seine Gedanken zu diesem Tool teilt.",
|
||||||
"shareExperience": "Teile deine Erfahrung mit {{name}}",
|
"shareExperience": "Teile deine Erfahrung mit {{name}}",
|
||||||
"score": "Punktestand",
|
"score": "Punktestand",
|
||||||
@@ -304,7 +306,6 @@
|
|||||||
"parameter": "Parameter",
|
"parameter": "Parameter",
|
||||||
"noResults": "Keine Treffer",
|
"noResults": "Keine Treffer",
|
||||||
"fields": "Felder",
|
"fields": "Felder",
|
||||||
"fieldHelpHint": "Hinweis: Formular-Felder verlinken per ?-Icon direkt zu den jeweiligen Zeilen dieser Tabelle.",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"in": "In",
|
"in": "In",
|
||||||
"requestBody": "Request-Body"
|
"requestBody": "Request-Body"
|
||||||
@@ -402,7 +403,31 @@
|
|||||||
"moveToTrashAction": "In den Papierkorb verschieben",
|
"moveToTrashAction": "In den Papierkorb verschieben",
|
||||||
"toastMoved": "Tools in den Papierkorb verschoben",
|
"toastMoved": "Tools in den Papierkorb verschoben",
|
||||||
"toastMovedSub": "{{count}} Tool(s) in den Papierkorb verschoben.",
|
"toastMovedSub": "{{count}} Tool(s) in den Papierkorb verschoben.",
|
||||||
"toastMoveFailed": "Verschieben in den Papierkorb fehlgeschlagen"
|
"toastMoveFailed": "Verschieben in den Papierkorb fehlgeschlagen",
|
||||||
|
"importTitle": "Tools importieren",
|
||||||
|
"importSub": "Mehrere Tools auf einmal importieren — als CSV, JSON oder YAML (Premium-Feature).",
|
||||||
|
"importButton": "Importieren",
|
||||||
|
"importFormatAuto": "Auto",
|
||||||
|
"importFormatAutoSub": "Das Format wird automatisch erkannt (CSV, JSON oder YAML).",
|
||||||
|
"importDelimiter": "CSV-Trenner",
|
||||||
|
"importDelimiterAuto": "Automatisch",
|
||||||
|
"importDelimiterComma": "Komma",
|
||||||
|
"importDelimiterSemicolon": "Semikolon",
|
||||||
|
"importDelimiterTab": "Tabulator",
|
||||||
|
"importCsvSub": "Spalten: name, description, category, websiteUrl, iconUrl, features, tags. Features/Tags durch | getrennt. Erste Zeile enthält die Spaltennamen.",
|
||||||
|
"importJsonSub": "JSON-Objekt oder Array von Objekten mit den Feldern name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||||
|
"importYamlSub": "YAML-Liste von Objekten (oder ein einzelnes Objekt) mit den Feldern name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||||
|
"importFile": "Datei laden",
|
||||||
|
"importTextareaPlaceholder": "name;description;category;features;tags\nMein Tool;Eine tolle Beschreibung;Monitoring;Deployment|Open Source;devops",
|
||||||
|
"importTextareaPlaceholderCsv": "name;description;category;features;tags\nMein Tool;Eine tolle Beschreibung;Monitoring;Deployment|Open Source;devops",
|
||||||
|
"importTextareaPlaceholderJson": "[\n {\n \"name\": \"Mein Tool\",\n \"description\": \"Eine tolle Beschreibung\",\n \"category\": \"Monitoring\",\n \"features\": [\"Deployment\", \"Open Source\"],\n \"tags\": [\"devops\"]\n }\n]",
|
||||||
|
"importTextareaPlaceholderYaml": "- name: Mein Tool\n description: Eine tolle Beschreibung\n category: Monitoring\n features:\n - Deployment\n - Open Source\n tags:\n - devops",
|
||||||
|
"importTextareaPlaceholderAuto": "name;description;category\nMein Tool;Eine tolle Beschreibung;Monitoring",
|
||||||
|
"importing": "Wird importiert…",
|
||||||
|
"importDone": "{{count}} von {{total}} Tools erfolgreich importiert.",
|
||||||
|
"importErrors": "{{count}} Zeilen mit Fehlern:",
|
||||||
|
"importErrorRow": "Zeile {{row}}",
|
||||||
|
"toastImportDone": "Tools erfolgreich importiert"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Plattform-Analysen",
|
"title": "Plattform-Analysen",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"viewDetails": "View details",
|
"viewDetails": "View details",
|
||||||
"guideTooltip": "Guide in the documentation",
|
"guideTooltip": "Guide in the documentation",
|
||||||
|
"help": "Help",
|
||||||
"viewMode": "View mode",
|
"viewMode": "View mode",
|
||||||
"viewGrid": "Grid",
|
"viewGrid": "Grid",
|
||||||
"viewTable": "Table",
|
"viewTable": "Table",
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"welcome": "Welcome to toolr",
|
"welcome": "Welcome to toolr",
|
||||||
"tagline": "The community hub where engineers honestly rate the tools they use daily.",
|
"tagline": "Rate your Tool.",
|
||||||
"totalTools": "Total Tools",
|
"totalTools": "Total Tools",
|
||||||
"totalRatings": "Total Ratings",
|
"totalRatings": "Total Ratings",
|
||||||
"avgRating": "Avg Rating",
|
"avgRating": "Avg Rating",
|
||||||
@@ -100,6 +101,7 @@
|
|||||||
"compareCount": "Compare ({{count}})",
|
"compareCount": "Compare ({{count}})",
|
||||||
"addToCompare": "Add to compare",
|
"addToCompare": "Add to compare",
|
||||||
"removeFromCompare": "Remove from compare",
|
"removeFromCompare": "Remove from compare",
|
||||||
|
"compareLimitReached": "A maximum of 9 tools can be compared.",
|
||||||
"comparePremiumTitle": "Compare is a Premium feature",
|
"comparePremiumTitle": "Compare is a Premium feature",
|
||||||
"comparePremiumSub": "Comparing tools side-by-side is available to Premium and Enterprise users. Upgrade your plan to unlock it.",
|
"comparePremiumSub": "Comparing tools side-by-side is available to Premium and Enterprise users. Upgrade your plan to unlock it.",
|
||||||
"notNow": "Not now",
|
"notNow": "Not now",
|
||||||
@@ -188,7 +190,7 @@
|
|||||||
"nameOptional": "Name (Optional)",
|
"nameOptional": "Name (Optional)",
|
||||||
"nameLabel": "Name",
|
"nameLabel": "Name",
|
||||||
"anonymousPlaceholder": "Anonymous",
|
"anonymousPlaceholder": "Anonymous",
|
||||||
"anonymousEngineer": "Anonymous Engineer",
|
"anonymousEngineer": "Anonymous User",
|
||||||
"beFirstToReview": "Be the first to share your thoughts on this tool.",
|
"beFirstToReview": "Be the first to share your thoughts on this tool.",
|
||||||
"shareExperience": "Share your experience with {{name}}",
|
"shareExperience": "Share your experience with {{name}}",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
@@ -304,7 +306,6 @@
|
|||||||
"parameter": "Parameter",
|
"parameter": "Parameter",
|
||||||
"noResults": "No results",
|
"noResults": "No results",
|
||||||
"fields": "Fields",
|
"fields": "Fields",
|
||||||
"fieldHelpHint": "Note: form fields link via the ? icon directly to the respective rows of this table.",
|
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"in": "In",
|
"in": "In",
|
||||||
"requestBody": "Request Body"
|
"requestBody": "Request Body"
|
||||||
@@ -402,7 +403,31 @@
|
|||||||
"moveToTrashAction": "Move to trash",
|
"moveToTrashAction": "Move to trash",
|
||||||
"toastMoved": "Tools moved to trash",
|
"toastMoved": "Tools moved to trash",
|
||||||
"toastMovedSub": "{{count}} tool(s) moved to trash.",
|
"toastMovedSub": "{{count}} tool(s) moved to trash.",
|
||||||
"toastMoveFailed": "Failed to move to trash"
|
"toastMoveFailed": "Failed to move to trash",
|
||||||
|
"importTitle": "Import tools",
|
||||||
|
"importSub": "Import multiple tools at once — as CSV, JSON or YAML (premium feature).",
|
||||||
|
"importButton": "Import",
|
||||||
|
"importFormatAuto": "Auto",
|
||||||
|
"importFormatAutoSub": "The format is detected automatically (CSV, JSON or YAML).",
|
||||||
|
"importDelimiter": "CSV delimiter",
|
||||||
|
"importDelimiterAuto": "Auto-detect",
|
||||||
|
"importDelimiterComma": "Comma",
|
||||||
|
"importDelimiterSemicolon": "Semicolon",
|
||||||
|
"importDelimiterTab": "Tab",
|
||||||
|
"importCsvSub": "Columns: name, description, category, websiteUrl, iconUrl, features, tags. Separate features/tags with |. The first line holds the column names.",
|
||||||
|
"importJsonSub": "A JSON object or array of objects with the fields name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||||
|
"importYamlSub": "A YAML list of objects (or a single object) with the fields name, description, category, websiteUrl, iconUrl, features, tags.",
|
||||||
|
"importFile": "Upload file",
|
||||||
|
"importTextareaPlaceholder": "name;description;category;features;tags\nMy Tool;A great description;Monitoring;Deployment|Open Source;devops",
|
||||||
|
"importTextareaPlaceholderCsv": "name;description;category;features;tags\nMy Tool;A great description;Monitoring;Deployment|Open Source;devops",
|
||||||
|
"importTextareaPlaceholderJson": "[\n {\n \"name\": \"My Tool\",\n \"description\": \"A great description\",\n \"category\": \"Monitoring\",\n \"features\": [\"Deployment\", \"Open Source\"],\n \"tags\": [\"devops\"]\n }\n]",
|
||||||
|
"importTextareaPlaceholderYaml": "- name: My Tool\n description: A great description\n category: Monitoring\n features:\n - Deployment\n - Open Source\n tags:\n - devops",
|
||||||
|
"importTextareaPlaceholderAuto": "name;description;category\nMy Tool;A great description;Monitoring",
|
||||||
|
"importing": "Importing…",
|
||||||
|
"importDone": "{{count}} of {{total}} tools imported successfully.",
|
||||||
|
"importErrors": "{{count}} rows with errors:",
|
||||||
|
"importErrorRow": "Row {{row}}",
|
||||||
|
"toastImportDone": "Tools imported successfully"
|
||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"title": "Platform Analytics",
|
"title": "Platform Analytics",
|
||||||
|
|||||||
@@ -123,6 +123,10 @@
|
|||||||
--chart-4: 43 100% 50%;
|
--chart-4: 43 100% 50%;
|
||||||
--chart-5: 280 80% 50%;
|
--chart-5: 280 80% 50%;
|
||||||
|
|
||||||
|
--glow-1: 187 100% 50%; /* cyan */
|
||||||
|
--glow-2: 265 100% 68%; /* violet */
|
||||||
|
--glow-3: 14 100% 57%; /* orange (primary) */
|
||||||
|
|
||||||
--app-font-sans: 'Inter', sans-serif;
|
--app-font-sans: 'Inter', sans-serif;
|
||||||
--app-font-serif: Georgia, serif;
|
--app-font-serif: Georgia, serif;
|
||||||
--app-font-mono: 'JetBrains Mono', Menlo, monospace;
|
--app-font-mono: 'JetBrains Mono', Menlo, monospace;
|
||||||
@@ -200,8 +204,8 @@
|
|||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
--popover-border: 217 33% 17%;
|
--popover-border: 217 33% 17%;
|
||||||
|
|
||||||
--primary: 38 92% 50%;
|
--primary: 187 95% 55%;
|
||||||
--primary-foreground: 222 47% 11%;
|
--primary-foreground: 210 40% 5%;
|
||||||
|
|
||||||
--secondary: 217 33% 17%;
|
--secondary: 217 33% 17%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
@@ -212,12 +216,12 @@
|
|||||||
--accent: 217 33% 17%;
|
--accent: 217 33% 17%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 72% 48%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--input: 217 33% 17%;
|
--input: 217 33% 17%;
|
||||||
--ring: 38 92% 50%;
|
--ring: 187 95% 55%;
|
||||||
--chart-1: 38 92% 50%;
|
--chart-1: 187 95% 55%;
|
||||||
--chart-2: 210 40% 98%;
|
--chart-2: 210 40% 98%;
|
||||||
--chart-3: 187 100% 42%;
|
--chart-3: 187 100% 42%;
|
||||||
--chart-4: 43 100% 50%;
|
--chart-4: 43 100% 50%;
|
||||||
@@ -372,4 +376,124 @@
|
|||||||
.border.hover-elevate:not(.no-hover-interaction-elevate)::after {
|
.border.hover-elevate:not(.no-hover-interaction-elevate)::after {
|
||||||
inset: -1px;
|
inset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Neon / AI glow system
|
||||||
|
* Dezent: funktioniert in Light + Dark. Animationen respektieren
|
||||||
|
* prefers-reduced-motion.
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
.neon-glow {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px hsl(var(--glow-1) / 0.35),
|
||||||
|
0 0 18px -2px hsl(var(--glow-1) / 0.45),
|
||||||
|
0 0 42px -8px hsl(var(--glow-2) / 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-glow-primary {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px hsl(var(--primary) / 0.4),
|
||||||
|
0 0 16px -2px hsl(var(--primary) / 0.38),
|
||||||
|
0 0 34px -8px hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Solid neon fill for progress/rating bars — high contrast, cyan → violet */
|
||||||
|
.neon-bar {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
100deg,
|
||||||
|
hsl(var(--glow-1) / 0.95) 0%,
|
||||||
|
hsl(var(--glow-2) / 0.95) 60%,
|
||||||
|
hsl(var(--glow-1) / 0.95) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
0 0 8px 0 hsl(var(--glow-1) / 0.45),
|
||||||
|
0 0 18px -2px hsl(var(--glow-2) / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated gradient background for loading states (Google-AI style flow) */
|
||||||
|
.loading-gradient {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
100deg,
|
||||||
|
hsl(var(--glow-3) / 0.14) 0%,
|
||||||
|
hsl(var(--glow-2) / 0.22) 25%,
|
||||||
|
hsl(var(--glow-1) / 0.24) 50%,
|
||||||
|
hsl(var(--glow-2) / 0.22) 75%,
|
||||||
|
hsl(var(--glow-3) / 0.14) 100%
|
||||||
|
);
|
||||||
|
background-size: 300% 100%;
|
||||||
|
animation: neon-loading-sweep 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-gradient-text {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
100deg,
|
||||||
|
hsl(var(--glow-3)) 0%,
|
||||||
|
hsl(var(--glow-1)) 45%,
|
||||||
|
hsl(var(--glow-2)) 75%,
|
||||||
|
hsl(var(--glow-3)) 100%
|
||||||
|
);
|
||||||
|
background-size: 250% 100%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
animation: neon-loading-sweep 8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-pulse {
|
||||||
|
animation: neon-pulse 3.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-spin {
|
||||||
|
animation: neon-rotate 1.4s linear infinite;
|
||||||
|
background: linear-gradient(
|
||||||
|
hsl(var(--glow-3)),
|
||||||
|
hsl(var(--glow-1)),
|
||||||
|
hsl(var(--glow-2)),
|
||||||
|
hsl(var(--glow-3))
|
||||||
|
);
|
||||||
|
animation: neon-rotate 1.4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-loading-sweep {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-pulse {
|
||||||
|
0%, 100% { opacity: 0.55; box-shadow: 0 0 6px -1px hsl(var(--glow-1) / 0.25); }
|
||||||
|
50% { opacity: 1; box-shadow: 0 0 16px 0 hsl(var(--glow-1) / 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left neon indicator for the active sidebar item */
|
||||||
|
.neon-sidebar-active {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.neon-sidebar-active[data-active="true"]::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 20%;
|
||||||
|
bottom: 20%;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--glow-1)),
|
||||||
|
hsl(var(--glow-2))
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 10px 1px hsl(var(--glow-1) / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.loading-gradient { animation: none; }
|
||||||
|
.loading-gradient-text { animation: none; }
|
||||||
|
.neon-pulse { animation: none; }
|
||||||
|
.neon-spin { animation: none; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench, Server } from "lucide-react";
|
import { Users, ScrollText, ShieldAlert, Plus, Pencil, Trash2, Clock, AlertTriangle, Wrench, Server } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { AdminToolsTab } from "@/components/admin-tools-tab";
|
import { AdminToolsTab } from "@/components/admin-tools-tab";
|
||||||
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
@@ -359,8 +360,9 @@ export default function Admin() {
|
|||||||
|
|
||||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||||
<DialogTitle>{t("admin.createNewUser")}</DialogTitle>
|
<DialogTitle className="min-w-0 truncate">{t("admin.createNewUser")}</DialogTitle>
|
||||||
|
<GuideHelp guide="administration" label={t("admin.createNewUser")} />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -401,7 +403,7 @@ export default function Admin() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter className="flex-col-reverse sm:flex-row sm:flex-wrap sm:justify-end gap-2">
|
||||||
<Button variant="outline" onClick={() => setCreateOpen(false)}>{t("common.cancel")}</Button>
|
<Button variant="outline" onClick={() => setCreateOpen(false)}>{t("common.cancel")}</Button>
|
||||||
<Button onClick={handleCreateUser} disabled={createUser.isPending || !newUsername || !newPassword}>
|
<Button onClick={handleCreateUser} disabled={createUser.isPending || !newUsername || !newPassword}>
|
||||||
{createUser.isPending ? t("admin.creating") : t("admin.createUser")}
|
{createUser.isPending ? t("admin.creating") : t("admin.createUser")}
|
||||||
@@ -411,9 +413,10 @@ export default function Admin() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) { setEditUser(null); setEditPassword(""); } }}>
|
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) { setEditUser(null); setEditPassword(""); } }}>
|
||||||
<DialogContent className="sm:max-w-sm">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader className="flex flex-row items-center justify-between gap-2">
|
||||||
<DialogTitle>{t("admin.editUser", { username: editUser?.username })}</DialogTitle>
|
<DialogTitle className="min-w-0 truncate">{t("admin.editUser", { username: editUser?.username })}</DialogTitle>
|
||||||
|
<GuideHelp guide="administration" label={t("admin.editUser", { username: editUser?.username })} />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -464,7 +467,7 @@ export default function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex-col sm:flex-row sm:justify-end gap-2">
|
<DialogFooter className="flex-col-reverse sm:flex-row sm:flex-wrap sm:justify-end gap-2">
|
||||||
<Button variant="outline" onClick={() => { setEditUser(null); setEditPassword(""); }}>{t("common.cancel")}</Button>
|
<Button variant="outline" onClick={() => { setEditUser(null); setEditPassword(""); }}>{t("common.cancel")}</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleUpdateUser}
|
onClick={handleUpdateUser}
|
||||||
@@ -495,7 +498,7 @@ export default function Admin() {
|
|||||||
</p>
|
</p>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>{t("common.cancel")}</Button>
|
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>{t("common.cancel")}</Button>
|
||||||
<Button variant="destructive" onClick={handleDeleteUser} disabled={deleteUser.isPending}>
|
<Button variant="outline" className="text-destructive hover:bg-destructive hover:text-destructive-foreground" onClick={handleDeleteUser} disabled={deleteUser.isPending}>
|
||||||
{deleteUser.isPending ? t("detail.deleting") : t("common.delete")}
|
{deleteUser.isPending ? t("detail.deleting") : t("common.delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@workspace/api-client-react";
|
} from "@workspace/api-client-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
@@ -51,9 +50,8 @@ export default function Analytics() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("analytics.title")}
|
{t("analytics.title")}
|
||||||
<GuideHelp guide="analytics" label={t("analytics.title")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("analytics.subtitle")}</p>
|
<p className="text-muted-foreground">{t("analytics.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,9 +127,15 @@ export default function Analytics() {
|
|||||||
<YAxis domain={[0, 5]} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
<YAxis domain={[0, 5]} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
||||||
<RechartsTooltip
|
<RechartsTooltip
|
||||||
cursor={{ fill: 'hsl(var(--muted))' }}
|
cursor={{ fill: 'hsl(var(--muted))' }}
|
||||||
contentStyle={{ borderRadius: '8px', border: '1px solid hsl(var(--border))' }}
|
contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '8px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="score" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={50} />
|
<defs>
|
||||||
|
<linearGradient id="neonBarFillA" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
|
||||||
|
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<Bar dataKey="score" fill="url(#neonBarFillA)" radius={[4, 4, 0, 0]} maxBarSize={50} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +160,7 @@ export default function Analytics() {
|
|||||||
<PolarAngleAxis dataKey="category" tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
<PolarAngleAxis dataKey="category" tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
||||||
<PolarRadiusAxis angle={30} domain={[0, 'auto']} tick={false} axisLine={false} />
|
<PolarRadiusAxis angle={30} domain={[0, 'auto']} tick={false} axisLine={false} />
|
||||||
<Radar name={t("analytics.radarTools")} dataKey="tools" stroke="hsl(var(--primary))" fill="hsl(var(--primary))" fillOpacity={0.3} />
|
<Radar name={t("analytics.radarTools")} dataKey="tools" stroke="hsl(var(--primary))" fill="hsl(var(--primary))" fillOpacity={0.3} />
|
||||||
<RechartsTooltip />
|
<RechartsTooltip contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '8px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
|
||||||
</RadarChart>
|
</RadarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,8 +187,14 @@ export default function Analytics() {
|
|||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
||||||
<YAxis hide />
|
<YAxis hide />
|
||||||
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
|
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '6px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
|
||||||
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={40} />
|
<defs>
|
||||||
|
<linearGradient id="neonBarFillB" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
|
||||||
|
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<Bar dataKey="count" fill="url(#neonBarFillB)" radius={[4, 4, 0, 0]} maxBarSize={40} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,8 +207,14 @@ export default function Analytics() {
|
|||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||||
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
||||||
<YAxis hide />
|
<YAxis hide />
|
||||||
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
|
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '6px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
|
||||||
<Bar dataKey="count" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} maxBarSize={40} />
|
<defs>
|
||||||
|
<linearGradient id="neonBarFillC" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
|
||||||
|
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<Bar dataKey="count" fill="url(#neonBarFillC)" radius={[4, 4, 0, 0]} maxBarSize={40} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Link, useSearch } from "wouter";
|
|||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { RatingStars } from "@/components/rating-stars";
|
import { RatingStars } from "@/components/rating-stars";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -102,9 +101,8 @@ export default function Compare() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("compare.title")}
|
{t("compare.title")}
|
||||||
<GuideHelp guide="vergleichen" label={t("compare.title")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("compare.subtitle", { count: list.length })}</p>
|
<p className="text-muted-foreground">{t("compare.subtitle", { count: list.length })}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
HelpCircle,
|
|
||||||
Library,
|
Library,
|
||||||
Menu,
|
Menu,
|
||||||
Search,
|
Search,
|
||||||
@@ -591,10 +590,6 @@ function SchemaView({ schema }: { schema: SchemaModel }) {
|
|||||||
{schema.description && <p className="text-muted-foreground">{schema.description}</p>}
|
{schema.description && <p className="text-muted-foreground">{schema.description}</p>}
|
||||||
</div>
|
</div>
|
||||||
<FieldTable fields={schema.fields} />
|
<FieldTable fields={schema.fields} />
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
<HelpCircle className="h-3.5 w-3.5 inline mr-1" />
|
|
||||||
{t("docs.fieldHelpHint")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Toc headings={headings} title={t("docs.fields")} />
|
<Toc headings={headings} title={t("docs.fields")} />
|
||||||
</div>
|
</div>
|
||||||
@@ -745,7 +740,7 @@ function ReleasesView({ versions }: { versions: ReleaseDoc[] }) {
|
|||||||
|
|
||||||
function ReleaseNoteView({ version, doc }: { version: string; doc: ReleaseDoc | null }) {
|
function ReleaseNoteView({ version, doc }: { version: string; doc: ReleaseDoc | null }) {
|
||||||
const locale = useDocsLocale();
|
const locale = useDocsLocale();
|
||||||
const file = doc ? `releases/${localizedFile(doc.file, doc.fileEn, locale)}` : `releases/${version}.md`;
|
const file = doc ? `releases/${localizedFile(doc.file, doc.fileEn, locale)}` : `releases/${version}.de.md`;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
<div className="flex-1 min-w-0 max-w-3xl">
|
<div className="flex-1 min-w-0 max-w-3xl">
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { ToolCard } from "@/components/tool-card";
|
import { ToolCard } from "@/components/tool-card";
|
||||||
import { Star, Wrench, MessageSquare, ArrowRight, Plus } from "lucide-react";
|
import { Star, Wrench, MessageSquare, ArrowRight, Plus } from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
@@ -25,9 +24,8 @@ export default function Home() {
|
|||||||
<div className="space-y-8 pb-8">
|
<div className="space-y-8 pb-8">
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("home.welcome")}
|
{t("home.welcome")}
|
||||||
<GuideHelp guide="getting-started" label={t("home.welcome")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("home.tagline")}</p>
|
<p className="text-muted-foreground">{t("home.tagline")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +39,7 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Stats Banner */}
|
{/* Stats Banner */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card className="bg-primary/5 border-primary/20">
|
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
|
||||||
<CardContent className="p-6 flex items-center gap-4">
|
<CardContent className="p-6 flex items-center gap-4">
|
||||||
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
||||||
<Wrench className="w-6 h-6" />
|
<Wrench className="w-6 h-6" />
|
||||||
@@ -57,7 +55,7 @@ export default function Home() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-primary/5 border-primary/20">
|
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
|
||||||
<CardContent className="p-6 flex items-center gap-4">
|
<CardContent className="p-6 flex items-center gap-4">
|
||||||
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
||||||
<MessageSquare className="w-6 h-6" />
|
<MessageSquare className="w-6 h-6" />
|
||||||
@@ -73,7 +71,7 @@ export default function Home() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-primary/5 border-primary/20">
|
<Card className="neon-glow-primary bg-primary/5 border-primary/20">
|
||||||
<CardContent className="p-6 flex items-center gap-4">
|
<CardContent className="p-6 flex items-center gap-4">
|
||||||
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
<div className="p-3 bg-primary/10 rounded-lg text-primary">
|
||||||
<Star className="w-6 h-6" />
|
<Star className="w-6 h-6" />
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function Login() {
|
|||||||
<p className="text-muted-foreground text-sm">{t("auth.loginSubtitle")}</p>
|
<p className="text-muted-foreground text-sm">{t("auth.loginSubtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card className="neon-glow">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
<CardTitle className="text-lg">{t("auth.signIn")}</CardTitle>
|
<CardTitle className="text-lg">{t("auth.signIn")}</CardTitle>
|
||||||
<CardDescription>{t("auth.loginDescription")}</CardDescription>
|
<CardDescription>{t("auth.loginDescription")}</CardDescription>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -47,10 +46,7 @@ export default function RedundancyPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AlertTriangle className="w-6 h-6 text-amber-500" />
|
<AlertTriangle className="w-6 h-6 text-amber-500" />
|
||||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
<h1 className="text-3xl font-bold">{t("redundancy.title")}</h1>
|
||||||
{t("redundancy.title")}
|
|
||||||
<GuideHelp guide="redundanz" label={t("redundancy.title")} />
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t("redundancy.subtitle")}
|
{t("redundancy.subtitle")}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { customFetch } from "@workspace/api-client-react";
|
import { customFetch } from "@workspace/api-client-react";
|
||||||
import { recordRecentTool } from "@/lib/recent-tools";
|
import { recordRecentTool } from "@/lib/recent-tools";
|
||||||
import { FieldHelp } from "@/components/field-help";
|
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
|
|
||||||
const ratingSchema = z.object({
|
const ratingSchema = z.object({
|
||||||
@@ -616,9 +615,8 @@ export default function ToolDetail() {
|
|||||||
<Dialog open={costDialogOpen} onOpenChange={(o) => { if (!o) setCostDialogOpen(false); }}>
|
<Dialog open={costDialogOpen} onOpenChange={(o) => { if (!o) setCostDialogOpen(false); }}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle>
|
||||||
{editCost ? t("detail.edit") + " " + t("detail.costs") : t("detail.addCost")}
|
{editCost ? t("detail.edit") + " " + t("detail.costs") : t("detail.addCost")}
|
||||||
<GuideHelp guide="kosten" label={t("detail.costs")} />
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>{t("detail.costDialogSub")}</DialogDescription>
|
<DialogDescription>{t("detail.costDialogSub")}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -719,8 +717,14 @@ export default function ToolDetail() {
|
|||||||
<BarChart data={usefulnessData} layout="vertical" margin={{ top: 0, right: 0, bottom: 0, left: -20 }}>
|
<BarChart data={usefulnessData} layout="vertical" margin={{ top: 0, right: 0, bottom: 0, left: -20 }}>
|
||||||
<XAxis type="number" hide />
|
<XAxis type="number" hide />
|
||||||
<YAxis dataKey="score" type="category" axisLine={false} tickLine={false} tickFormatter={(val) => `${val} ★`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
<YAxis dataKey="score" type="category" axisLine={false} tickLine={false} tickFormatter={(val) => `${val} ★`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
|
||||||
<Tooltip cursor={{ fill: 'transparent' }} />
|
<Tooltip cursor={{ fill: 'transparent' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '8px', color: 'hsl(var(--foreground))' }} />
|
||||||
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[0, 4, 4, 0]} barSize={12} />
|
<defs>
|
||||||
|
<linearGradient id="neonBarFill" x1="0" y1="0" x2="1" y2="0">
|
||||||
|
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
|
||||||
|
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<Bar dataKey="count" fill="url(#neonBarFill)" radius={[0, 4, 4, 0]} barSize={12} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -742,7 +746,7 @@ export default function ToolDetail() {
|
|||||||
<CartesianGrid strokeDasharray="3 3" opacity={0.2} />
|
<CartesianGrid strokeDasharray="3 3" opacity={0.2} />
|
||||||
<XAxis dataKey="date" hide />
|
<XAxis dataKey="date" hide />
|
||||||
<YAxis domain={[0, 5]} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} axisLine={false} tickLine={false} />
|
<YAxis domain={[0, 5]} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||||
<Tooltip labelFormatter={(_, payload) => (payload?.[0] ? format(new Date(payload[0].payload.date), "dd.MM.yyyy HH:mm") : "")} />
|
<Tooltip labelFormatter={(_, payload) => (payload?.[0] ? format(new Date(payload[0].payload.date), "dd.MM.yyyy HH:mm") : "")} contentStyle={{ backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '8px', color: 'hsl(var(--foreground))' }} />
|
||||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
<Line type="monotone" dataKey="combined" name={t("detail.score")} stroke="hsl(var(--primary))" strokeWidth={2} dot={false} />
|
<Line type="monotone" dataKey="combined" name={t("detail.score")} stroke="hsl(var(--primary))" strokeWidth={2} dot={false} />
|
||||||
<Line type="monotone" dataKey="usefulness" name={t("detail.usefulness")} stroke="hsl(var(--success))" strokeWidth={1.5} dot={false} strokeDasharray="4 3" />
|
<Line type="monotone" dataKey="usefulness" name={t("detail.usefulness")} stroke="hsl(var(--success))" strokeWidth={1.5} dot={false} strokeDasharray="4 3" />
|
||||||
@@ -768,12 +772,12 @@ export default function ToolDetail() {
|
|||||||
|
|
||||||
{isReviewFormOpen && (
|
{isReviewFormOpen && (
|
||||||
<Card className="border-primary shadow-sm">
|
<Card className="border-primary shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<div className="space-y-1.5">
|
||||||
{t("detail.addReview")}
|
<CardTitle>{t("detail.addReview")}</CardTitle>
|
||||||
<GuideHelp guide="bewerten" label={t("detail.addReview")} />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>{t("detail.shareExperience", { name: tool.name })}</CardDescription>
|
<CardDescription>{t("detail.shareExperience", { name: tool.name })}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<GuideHelp guide="bewerten" label={t("detail.addReview")} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -786,7 +790,6 @@ export default function ToolDetail() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("detail.usefulness")}
|
{t("detail.usefulness")}
|
||||||
<FieldHelp schema="RatingInput" field="usefulness">{t("detail.usefulness")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -807,7 +810,6 @@ export default function ToolDetail() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("detail.usability")}
|
{t("detail.usability")}
|
||||||
<FieldHelp schema="RatingInput" field="usability">{t("detail.usability")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -830,7 +832,6 @@ export default function ToolDetail() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("detail.commentOptional")}
|
{t("detail.commentOptional")}
|
||||||
<FieldHelp schema="RatingInput" field="comment">{t("detail.commentLabel")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -851,7 +852,6 @@ export default function ToolDetail() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("detail.nameOptional")}
|
{t("detail.nameOptional")}
|
||||||
<FieldHelp schema="RatingInput" field="reviewerName">{t("detail.nameLabel")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t("detail.anonymousPlaceholder")} {...field} />
|
<Input placeholder={t("detail.anonymousPlaceholder")} {...field} />
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
|||||||
import { FeatureInput } from "@/components/feature-input";
|
import { FeatureInput } from "@/components/feature-input";
|
||||||
import { TagInput } from "@/components/tag-input";
|
import { TagInput } from "@/components/tag-input";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { FieldHelp } from "@/components/field-help";
|
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -163,13 +162,15 @@ export default function ToolEdit() {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Pencil className="w-5 h-5 text-primary" />
|
<Pencil className="w-5 h-5 text-primary" />
|
||||||
{t("toolForm.toolDetails")}
|
{t("toolForm.toolDetails")}
|
||||||
<GuideHelp guide="tool-bearbeiten" label={t("toolForm.toolDetails")} />
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{t("toolForm.toolDetailsEditSub")}</CardDescription>
|
<CardDescription>{t("toolForm.toolDetailsEditSub")}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<GuideHelp guide="tool-bearbeiten" label={t("toolForm.toolDetails")} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -182,7 +183,6 @@ export default function ToolEdit() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.name")}
|
{t("toolForm.name")}
|
||||||
<FieldHelp schema="ToolInput" field="name">{t("toolForm.name")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t("toolForm.name")} {...field} />
|
<Input placeholder={t("toolForm.name")} {...field} />
|
||||||
@@ -198,7 +198,6 @@ export default function ToolEdit() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.category")}
|
{t("toolForm.category")}
|
||||||
<FieldHelp schema="ToolInput" field="category">{t("toolForm.category")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CategoryCombobox value={field.value} onChange={field.onChange} />
|
<CategoryCombobox value={field.value} onChange={field.onChange} />
|
||||||
@@ -216,7 +215,6 @@ export default function ToolEdit() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.websiteUrlOptional")}
|
{t("toolForm.websiteUrlOptional")}
|
||||||
<FieldHelp schema="ToolInput" field="websiteUrl">{t("toolForm.websiteUrl")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t("toolForm.urlPlaceholder")} type="url" {...field} />
|
<Input placeholder={t("toolForm.urlPlaceholder")} type="url" {...field} />
|
||||||
@@ -233,7 +231,6 @@ export default function ToolEdit() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.iconUrlOptional")}
|
{t("toolForm.iconUrlOptional")}
|
||||||
<FieldHelp schema="ToolInput" field="iconUrl">{t("toolForm.iconUrl")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -269,7 +266,6 @@ export default function ToolEdit() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.description")}
|
{t("toolForm.description")}
|
||||||
<FieldHelp schema="ToolInput" field="description">{t("toolForm.description")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -288,7 +284,6 @@ export default function ToolEdit() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.features")}
|
{t("toolForm.features")}
|
||||||
<FieldHelp schema="ToolInput" field="features">{t("toolForm.features")}</FieldHelp>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t("toolForm.featuresEditSub")}</p>
|
<p className="text-sm text-muted-foreground">{t("toolForm.featuresEditSub")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +330,6 @@ export default function ToolEdit() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.tags")}
|
{t("toolForm.tags")}
|
||||||
<FieldHelp schema="ToolInput" field="tags">{t("toolForm.tags")}</FieldHelp>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t("toolForm.tagsEditSub")}</p>
|
<p className="text-sm text-muted-foreground">{t("toolForm.tagsEditSub")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { CategoryCombobox } from "@/components/category-combobox";
|
|||||||
import { FeatureInput } from "@/components/feature-input";
|
import { FeatureInput } from "@/components/feature-input";
|
||||||
import { TagInput } from "@/components/tag-input";
|
import { TagInput } from "@/components/tag-input";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { FieldHelp } from "@/components/field-help";
|
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
import { GuideHelp } from "@/components/guide-help";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -126,13 +125,15 @@ export default function ToolNew() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Wrench className="w-5 h-5 text-primary" />
|
<Wrench className="w-5 h-5 text-primary" />
|
||||||
{t("toolForm.toolDetails")}
|
{t("toolForm.toolDetails")}
|
||||||
<GuideHelp guide="tool-anlegen" label={t("toolForm.toolDetails")} />
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>{t("toolForm.toolDetailsNewSub")}</CardDescription>
|
<CardDescription>{t("toolForm.toolDetailsNewSub")}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<GuideHelp guide="tool-anlegen" label={t("toolForm.toolDetails")} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -145,7 +146,6 @@ export default function ToolNew() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.name")}
|
{t("toolForm.name")}
|
||||||
<FieldHelp schema="ToolInput" field="name">{t("toolForm.name")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t("toolForm.namePlaceholder")} {...field} data-testid="input-tool-name" />
|
<Input placeholder={t("toolForm.namePlaceholder")} {...field} data-testid="input-tool-name" />
|
||||||
@@ -162,7 +162,6 @@ export default function ToolNew() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.category")}
|
{t("toolForm.category")}
|
||||||
<FieldHelp schema="ToolInput" field="category">{t("toolForm.category")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CategoryCombobox
|
<CategoryCombobox
|
||||||
@@ -183,7 +182,6 @@ export default function ToolNew() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.websiteUrlOptional")}
|
{t("toolForm.websiteUrlOptional")}
|
||||||
<FieldHelp schema="ToolInput" field="websiteUrl">{t("toolForm.websiteUrl")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={t("toolForm.urlPlaceholder")} type="url" {...field} data-testid="input-tool-url" />
|
<Input placeholder={t("toolForm.urlPlaceholder")} type="url" {...field} data-testid="input-tool-url" />
|
||||||
@@ -200,7 +198,6 @@ export default function ToolNew() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.iconUrlOptional")}
|
{t("toolForm.iconUrlOptional")}
|
||||||
<FieldHelp schema="ToolInput" field="iconUrl">{t("toolForm.iconUrl")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -237,7 +234,6 @@ export default function ToolNew() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="inline-flex items-center gap-1.5">
|
<FormLabel className="inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.description")}
|
{t("toolForm.description")}
|
||||||
<FieldHelp schema="ToolInput" field="description">{t("toolForm.description")}</FieldHelp>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -257,7 +253,6 @@ export default function ToolNew() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.features")}
|
{t("toolForm.features")}
|
||||||
<FieldHelp schema="ToolInput" field="features">{t("toolForm.features")}</FieldHelp>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t("toolForm.featuresNewSub")}</p>
|
<p className="text-sm text-muted-foreground">{t("toolForm.featuresNewSub")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,7 +308,6 @@ export default function ToolNew() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
<h3 className="text-lg font-medium inline-flex items-center gap-1.5">
|
||||||
{t("toolForm.tags")}
|
{t("toolForm.tags")}
|
||||||
<FieldHelp schema="ToolInput" field="tags">{t("toolForm.tags")}</FieldHelp>
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t("toolForm.tagsHelp")}</p>
|
<p className="text-sm text-muted-foreground">{t("toolForm.tagsHelp")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
} from "@workspace/api-client-react";
|
} from "@workspace/api-client-react";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { ToolCard } from "@/components/tool-card";
|
import { ToolCard } from "@/components/tool-card";
|
||||||
import { ToolCardWide } from "@/components/tool-card-wide";
|
import { ToolCardWide } from "@/components/tool-card-wide";
|
||||||
import { ToolRow, TABLE_GRID } from "@/components/tool-row";
|
import { ToolRow, TABLE_GRID } from "@/components/tool-row";
|
||||||
@@ -19,6 +18,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|||||||
import { useBrowsePreferences, isViewMode, isDensity } from "@/hooks/use-browse-preferences";
|
import { useBrowsePreferences, isViewMode, isDensity } from "@/hooks/use-browse-preferences";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { CompareBar } from "@/components/compare-bar";
|
import { CompareBar } from "@/components/compare-bar";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -92,12 +92,20 @@ export default function ToolsBrowse() {
|
|||||||
const { serverView, serverDensity, localView, localDensity, persist } = useBrowsePreferences();
|
const { serverView, serverDensity, localView, localDensity, persist } = useBrowsePreferences();
|
||||||
const { hasFeature } = useAuth();
|
const { hasFeature } = useAuth();
|
||||||
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
|
const { isWatched, toggle: toggleWatchlist, canWatchlist } = useWatchlist();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [compareIds, setCompareIds] = useState<number[]>([]);
|
const [compareIds, setCompareIds] = useState<number[]>([]);
|
||||||
const [compareUpsellOpen, setCompareUpsellOpen] = useState(false);
|
const [compareUpsellOpen, setCompareUpsellOpen] = useState(false);
|
||||||
|
|
||||||
function toggleCompare(id: number) {
|
function toggleCompare(id: number) {
|
||||||
setCompareIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
setCompareIds((prev) => {
|
||||||
|
if (prev.includes(id)) return prev.filter((x) => x !== id);
|
||||||
|
if (prev.length >= 9) {
|
||||||
|
toast({ title: t("browse.compareLimitReached"), variant: "destructive" });
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, id];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [search, setSearch] = useState(initialSearch);
|
const [search, setSearch] = useState(initialSearch);
|
||||||
@@ -231,9 +239,8 @@ export default function ToolsBrowse() {
|
|||||||
<div className="space-y-6 pb-8">
|
<div className="space-y-6 pb-8">
|
||||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("nav.browseTools")}
|
{t("nav.browseTools")}
|
||||||
<GuideHelp guide="tools-finden" label={t("browse.subtitle")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("browse.subtitle")}</p>
|
<p className="text-muted-foreground">{t("browse.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -525,10 +532,7 @@ export default function ToolsBrowse() {
|
|||||||
<AlertDialog open={compareUpsellOpen} onOpenChange={setCompareUpsellOpen}>
|
<AlertDialog open={compareUpsellOpen} onOpenChange={setCompareUpsellOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle>{t("browse.comparePremiumTitle")}</AlertDialogTitle>
|
||||||
{t("browse.comparePremiumTitle")}
|
|
||||||
<GuideHelp guide="plaene" label={t("browse.comparePremiumTitle")} />
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{t("browse.comparePremiumSub")}
|
{t("browse.comparePremiumSub")}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -157,9 +156,8 @@ export default function Trash() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("trash.title")}
|
{t("trash.title")}
|
||||||
<GuideHelp guide="papierkorb" label={t("trash.title")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("trash.subtitle")}</p>
|
<p className="text-muted-foreground">{t("trash.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +192,8 @@ export default function Trash() {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="outline"
|
||||||
|
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
disabled={selectedIds.length === 0 || deletePermanent.isPending}
|
disabled={selectedIds.length === 0 || deletePermanent.isPending}
|
||||||
onClick={() => setConfirmDelete(true)}
|
onClick={() => setConfirmDelete(true)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useAuth } from "@/hooks/use-auth";
|
|||||||
import { useWatchlist } from "@/hooks/use-watchlist";
|
import { useWatchlist } from "@/hooks/use-watchlist";
|
||||||
import { Layout } from "@/components/layout";
|
import { Layout } from "@/components/layout";
|
||||||
import { ToolCard } from "@/components/tool-card";
|
import { ToolCard } from "@/components/tool-card";
|
||||||
import { GuideHelp } from "@/components/guide-help";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { ShieldAlert, Bookmark } from "lucide-react";
|
import { ShieldAlert, Bookmark } from "lucide-react";
|
||||||
@@ -43,9 +42,8 @@ export default function Watchlist() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<div className="space-y-6 pb-10">
|
<div className="space-y-6 pb-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2 flex items-center gap-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{t("watchlist.title")}
|
{t("watchlist.title")}
|
||||||
<GuideHelp guide="watchlist" label={t("watchlist.title")} />
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">{t("watchlist.subtitle")}</p>
|
<p className="text-muted-foreground">{t("watchlist.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# Beispiel-YAML zum Import in den Admin-Bereich (Tab „Tools" -> Importieren).
|
||||||
|
# Erwartetes Format pro Eintrag:
|
||||||
|
# name (Pflicht)
|
||||||
|
# description, category, websiteUrl, iconUrl (optional)
|
||||||
|
# features, tags (Listen oder | -getrennte Strings)
|
||||||
|
# Das Format wird automatisch erkannt; bei CSV den Trenner wählen.
|
||||||
|
# Fehlerhafte Zeilen werden übersprungen und mit Zeilennummer aufgelistet.
|
||||||
|
|
||||||
|
- name: Slack
|
||||||
|
description: Team-Kommunikation mit Channels, Direktnachrichten und App-Integrationen
|
||||||
|
category: Kommunikation
|
||||||
|
websiteUrl: https://slack.com
|
||||||
|
features:
|
||||||
|
- Channels
|
||||||
|
- Videoanrufe
|
||||||
|
- App-Integrationen
|
||||||
|
tags:
|
||||||
|
- communication
|
||||||
|
- teamwork
|
||||||
|
|
||||||
|
- name: Microsoft Teams
|
||||||
|
description: Chat, Besprechungen, Dateien und Zusammenarbeit in einer Plattform
|
||||||
|
category: Kommunikation
|
||||||
|
websiteUrl: https://www.microsoft.com/de-de/microsoft-365/microsoft-teams
|
||||||
|
features:
|
||||||
|
- Videokonferenzen
|
||||||
|
- Teamkanäle
|
||||||
|
- Office-Integration
|
||||||
|
tags:
|
||||||
|
- communication
|
||||||
|
- meetings
|
||||||
|
|
||||||
|
- name: Zoom
|
||||||
|
description: Videokonferenzen und Webinare für Teams und externe Partner
|
||||||
|
category: Kommunikation
|
||||||
|
websiteUrl: https://zoom.us
|
||||||
|
features:
|
||||||
|
- Videokonferenzen
|
||||||
|
- Webinare
|
||||||
|
- Bildschirmfreigabe
|
||||||
|
tags:
|
||||||
|
- video
|
||||||
|
- meetings
|
||||||
|
|
||||||
|
- name: Jira
|
||||||
|
description: Agile Projekt- und Issue-Verfolgung für Entwicklung und Betrieb
|
||||||
|
category: Projektmanagement
|
||||||
|
websiteUrl: https://www.atlassian.com/software/jira
|
||||||
|
features:
|
||||||
|
- Backlog
|
||||||
|
- Sprints
|
||||||
|
- Kanban-Boards
|
||||||
|
tags:
|
||||||
|
- agile
|
||||||
|
- project-management
|
||||||
|
|
||||||
|
- name: Confluence
|
||||||
|
description: Kollaborative Wissensdatenbank und Dokumentation für Teams
|
||||||
|
category: Projektmanagement
|
||||||
|
websiteUrl: https://www.atlassian.com/software/confluence
|
||||||
|
features:
|
||||||
|
- Wikis
|
||||||
|
- Seitenbausteine
|
||||||
|
- Berechtigungen
|
||||||
|
tags:
|
||||||
|
- documentation
|
||||||
|
- knowledge-base
|
||||||
|
|
||||||
|
- name: GitLab
|
||||||
|
description: DevOps-Plattform mit Git-Repositorys, CI/CD und Code-Review
|
||||||
|
category: Entwicklung
|
||||||
|
websiteUrl: https://about.gitlab.com
|
||||||
|
features:
|
||||||
|
- Git-Repositorys
|
||||||
|
- CI/CD
|
||||||
|
- Merge Requests
|
||||||
|
tags:
|
||||||
|
- devops
|
||||||
|
- git
|
||||||
|
|
||||||
|
- name: GitHub
|
||||||
|
description: Versionsverwaltung, Code-Review und Collaboration auf Git-Basis
|
||||||
|
category: Entwicklung
|
||||||
|
websiteUrl: https://github.com
|
||||||
|
features:
|
||||||
|
- Repositories
|
||||||
|
- Pull Requests
|
||||||
|
- Actions
|
||||||
|
tags:
|
||||||
|
- git
|
||||||
|
- collaboration
|
||||||
|
|
||||||
|
- name: Jenkins
|
||||||
|
description: Automatisierte Build- und Deploy-Pipelines für Softwareprojekte
|
||||||
|
category: CI/CD
|
||||||
|
websiteUrl: https://www.jenkins.io
|
||||||
|
features:
|
||||||
|
- Pipelines
|
||||||
|
- Plugins
|
||||||
|
- Job-Automatisierung
|
||||||
|
tags:
|
||||||
|
- ci
|
||||||
|
- cd
|
||||||
|
|
||||||
|
- name: Docker
|
||||||
|
description: Container-Plattform zum Paketieren und Ausführen von Anwendungen
|
||||||
|
category: Entwicklung
|
||||||
|
websiteUrl: https://www.docker.com
|
||||||
|
features:
|
||||||
|
- Container
|
||||||
|
- Images
|
||||||
|
- Compose
|
||||||
|
tags:
|
||||||
|
- containers
|
||||||
|
- virtualization
|
||||||
|
|
||||||
|
- name: Grafana
|
||||||
|
description: Observability- und Dashboard-Plattform für Metriken, Logs und Alerts
|
||||||
|
category: Monitoring
|
||||||
|
websiteUrl: https://grafana.com
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- Alerts
|
||||||
|
- Datenquellen
|
||||||
|
tags:
|
||||||
|
- observability
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
- name: Prometheus
|
||||||
|
description: Open-Source-Systeme zur Zeitreihen-Metrikerfassung und -Auswertung
|
||||||
|
category: Monitoring
|
||||||
|
websiteUrl: https://prometheus.io
|
||||||
|
features:
|
||||||
|
- Metriken
|
||||||
|
- Alerting
|
||||||
|
- Time-Series-Datenbank
|
||||||
|
tags:
|
||||||
|
- monitoring
|
||||||
|
- metrics
|
||||||
|
|
||||||
|
- name: Datadog
|
||||||
|
description: Cloud-basierte Observability für Infrastruktur, APM und Logs
|
||||||
|
category: Monitoring
|
||||||
|
websiteUrl: https://www.datadoghq.com
|
||||||
|
features:
|
||||||
|
- Infrastruktur-Monitoring
|
||||||
|
- APM
|
||||||
|
- Log-Management
|
||||||
|
tags:
|
||||||
|
- observability
|
||||||
|
- saas
|
||||||
|
|
||||||
|
- name: PagerDuty
|
||||||
|
description: Incident-Management und On-Call-Benachrichtigungen für Teams
|
||||||
|
category: Betrieb
|
||||||
|
websiteUrl: https://www.pagerduty.com
|
||||||
|
features:
|
||||||
|
- Alerting
|
||||||
|
- On-Call-Pläne
|
||||||
|
- Incident-Response
|
||||||
|
tags:
|
||||||
|
- incident-management
|
||||||
|
- on-call
|
||||||
|
|
||||||
|
- name: Sentry
|
||||||
|
description: Fehlerüberwachung und Performance-Monitoring für Anwendungen
|
||||||
|
category: Monitoring
|
||||||
|
websiteUrl: https://sentry.io
|
||||||
|
features:
|
||||||
|
- Error-Tracking
|
||||||
|
- Releases
|
||||||
|
- Performance
|
||||||
|
tags:
|
||||||
|
- error-monitoring
|
||||||
|
- debugging
|
||||||
|
|
||||||
|
- name: Terraform
|
||||||
|
description: Infrastructure-as-Code zur deklarativen Bereitstellung von Ressourcen
|
||||||
|
category: Infrastruktur
|
||||||
|
websiteUrl: https://www.terraform.io
|
||||||
|
features:
|
||||||
|
- Modules
|
||||||
|
- State-Management
|
||||||
|
- Provider
|
||||||
|
tags:
|
||||||
|
- iac
|
||||||
|
- infrastructure
|
||||||
|
|
||||||
|
- name: Ansible
|
||||||
|
description: Agentenlose Automatisierung für Konfiguration, Deploy und Orchestrierung
|
||||||
|
category: Infrastruktur
|
||||||
|
websiteUrl: https://www.ansible.com
|
||||||
|
features:
|
||||||
|
- Playbooks
|
||||||
|
- Inventories
|
||||||
|
- Modules
|
||||||
|
tags:
|
||||||
|
- automation
|
||||||
|
- configuration
|
||||||
|
|
||||||
|
- name: Elasticsearch
|
||||||
|
description: Verteilter Such- und Analytics-Server für Logs, Metriken und Daten
|
||||||
|
category: Datenbank
|
||||||
|
websiteUrl: https://www.elastic.co
|
||||||
|
features:
|
||||||
|
- Volltextsuche
|
||||||
|
- Aggregationen
|
||||||
|
- Skalierung
|
||||||
|
tags:
|
||||||
|
- search
|
||||||
|
- analytics
|
||||||
|
|
||||||
|
- name: Kubernetes
|
||||||
|
description: Container-Orchestrierung für Deployment, Skalierung und Betrieb
|
||||||
|
category: Infrastruktur
|
||||||
|
websiteUrl: https://kubernetes.io
|
||||||
|
features:
|
||||||
|
- Pods
|
||||||
|
- Services
|
||||||
|
- Autoskalierung
|
||||||
|
tags:
|
||||||
|
- containers
|
||||||
|
- orchestration
|
||||||
|
|
||||||
|
- name: Sonarqube
|
||||||
|
description: Statische Code-Analyse und Qualitätsmessung für Softwareprojekte
|
||||||
|
category: Entwicklung
|
||||||
|
websiteUrl: https://www.sonarsource.com/products/sonarqube
|
||||||
|
features:
|
||||||
|
- Code-Qualität
|
||||||
|
- Security-Scans
|
||||||
|
- Quality Gates
|
||||||
|
tags:
|
||||||
|
- code-quality
|
||||||
|
- static-analysis
|
||||||
|
|
||||||
|
- name: Power BI
|
||||||
|
description: Business-Intelligence-Tool für Dashboards, Berichte und Datenanalyse
|
||||||
|
category: Analytics
|
||||||
|
websiteUrl: https://powerbi.microsoft.com
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- Berichte
|
||||||
|
- Datenmodelle
|
||||||
|
tags:
|
||||||
|
- bi
|
||||||
|
- analytics
|
||||||
|
|
||||||
|
- name: Google Workspace
|
||||||
|
description: Produktivitäts-Suite mit E-Mail, Docs, Drive und Meet
|
||||||
|
category: Produktivität
|
||||||
|
websiteUrl: https://workspace.google.com
|
||||||
|
features:
|
||||||
|
- Gmail
|
||||||
|
- Google Docs
|
||||||
|
- Google Drive
|
||||||
|
tags:
|
||||||
|
- suite
|
||||||
|
- productivity
|
||||||
|
|
||||||
|
- name: Microsoft 365
|
||||||
|
description: Office-Anwendungen, E-Mail und Cloud-Dienste für Unternehmen
|
||||||
|
category: Produktivität
|
||||||
|
websiteUrl: https://www.microsoft.com/microsoft-365
|
||||||
|
features:
|
||||||
|
- Word
|
||||||
|
- Excel
|
||||||
|
- Outlook
|
||||||
|
tags:
|
||||||
|
- suite
|
||||||
|
- productivity
|
||||||
|
|
||||||
|
- name: Slack Enterprise Grid
|
||||||
|
description: Slack-Verwaltung für große Organisationen mit erweiterten Sicherheitsfunktionen
|
||||||
|
category: Kommunikation
|
||||||
|
websiteUrl: https://slack.com
|
||||||
|
features:
|
||||||
|
- Enterprise-Admin
|
||||||
|
- Compliance
|
||||||
|
- Multi-Workspace
|
||||||
|
tags:
|
||||||
|
- communication
|
||||||
|
- enterprise
|
||||||
|
|
||||||
|
- name: Atlassian Rovo
|
||||||
|
description: KI-Assistent für Suche und Zusammenarbeit über Atlassian-Produkte
|
||||||
|
category: Projektmanagement
|
||||||
|
websiteUrl: https://www.atlassian.com/software/rovo
|
||||||
|
features:
|
||||||
|
- KI-Suche
|
||||||
|
- Agenten
|
||||||
|
- Integrationen
|
||||||
|
tags:
|
||||||
|
- ai
|
||||||
|
- search
|
||||||
@@ -36,6 +36,49 @@ Zentraler Zugriff auf den Tool-Katalog.
|
|||||||
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
||||||
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
||||||
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
||||||
|
- **Importieren** (Premium): über die Schaltfläche **Importieren** oben rechts
|
||||||
|
können mehrere Tools gleichzeitig angelegt werden. Das Format wird automatisch
|
||||||
|
erkannt (CSV, JSON oder YAML); für CSV lässt sich der Trenner wählen
|
||||||
|
(automatisch, Komma, Semikolon oder Tabulator). Die Daten lassen sich direkt
|
||||||
|
ins Formular einfügen oder als Datei hochladen.
|
||||||
|
|
||||||
|
Unterstützte Felder: `name` (Pflicht), `description`, `category`, `websiteUrl`,
|
||||||
|
`iconUrl`, `features`, `tags` (`features`/`tags` sind mit `|` getrennte Listen).
|
||||||
|
|
||||||
|
**CSV-Beispiel:**
|
||||||
|
|
||||||
|
```csv
|
||||||
|
name;description;category;features;tags
|
||||||
|
Grafana;Observability- und Dashboard-Plattform;Monitoring;Dashboards|Alerts;devops
|
||||||
|
Prometheus;Zeitreihen-Datenbank;Monitoring;Metrics;devops
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON-Beispiel:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "Grafana", "category": "Monitoring", "features": ["Dashboards"] },
|
||||||
|
{ "name": "Prometheus", "category": "Monitoring" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**YAML-Beispiel:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Grafana
|
||||||
|
category: Monitoring
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- name: Prometheus
|
||||||
|
category: Monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
Eine umfangreichere, fertig importierbare Beispieldatei liegt unter
|
||||||
|
`docs/examples/import-tools.yaml` (24 gängige Enterprise-Tools).
|
||||||
|
|
||||||
|
Beim Import werden einzelne fehlerhafte Zeilen übersprungen und mit Zeilennummer
|
||||||
|
aufgelistet; korrekte Zeilen werden trotzdem angelegt. Jeder Import erzeugt
|
||||||
|
Audit-Log-Einträge.
|
||||||
|
|
||||||
## Tab „Audit-Log"
|
## Tab „Audit-Log"
|
||||||
|
|
||||||
@@ -36,6 +36,49 @@ Central access to the tool catalog.
|
|||||||
- **Bulk action:** select multiple tools and move them to the trash
|
- **Bulk action:** select multiple tools and move them to the trash
|
||||||
(confirmation dialog; soft-deleted tools are removed from all public views
|
(confirmation dialog; soft-deleted tools are removed from all public views
|
||||||
and can be restored or permanently deleted).
|
and can be restored or permanently deleted).
|
||||||
|
- **Import** (premium): via the **Import** button at the top right, multiple
|
||||||
|
tools can be created at once. The format is detected automatically (CSV,
|
||||||
|
JSON or YAML); for CSV the delimiter can be chosen (auto-detect, comma,
|
||||||
|
semicolon or tab). Data can be pasted directly into the form or uploaded
|
||||||
|
as a file.
|
||||||
|
|
||||||
|
Supported fields: `name` (required), `description`, `category`, `websiteUrl`,
|
||||||
|
`iconUrl`, `features`, `tags` (`features`/`tags` are `|`-separated lists).
|
||||||
|
|
||||||
|
**CSV example:**
|
||||||
|
|
||||||
|
```csv
|
||||||
|
name;description;category;features;tags
|
||||||
|
Grafana;Observability and dashboard platform;Monitoring;Dashboards|Alerts;devops
|
||||||
|
Prometheus;Time-series database;Monitoring;Metrics;devops
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "Grafana", "category": "Monitoring", "features": ["Dashboards"] },
|
||||||
|
{ "name": "Prometheus", "category": "Monitoring" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**YAML example:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Grafana
|
||||||
|
category: Monitoring
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- name: Prometheus
|
||||||
|
category: Monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
A larger, ready-to-import example file is located at
|
||||||
|
`docs/examples/import-tools.yaml` (24 common enterprise tools).
|
||||||
|
|
||||||
|
During import, individual faulty rows are skipped and listed with their row
|
||||||
|
number; correct rows are still created. Every import produces audit-log
|
||||||
|
entries.
|
||||||
|
|
||||||
## "Audit log" tab
|
## "Audit log" tab
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ order: 1
|
|||||||
# Willkommen bei toolr
|
# Willkommen bei toolr
|
||||||
|
|
||||||
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
||||||
Entwicklungstools**. Nutzer:innen pflegen einen gemeinsamen Katalog von Tools,
|
Entwicklungstools**. Nutzer pflegen einen gemeinsamen Katalog von Tools,
|
||||||
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
||||||
die richtige Wahl zu treffen.
|
die richtige Wahl zu treffen.
|
||||||
|
|
||||||
@@ -16,13 +16,14 @@ die richtige Wahl zu treffen.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
||||||
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
||||||
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller:in oder Admin) | Angemeldet |
|
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller oder Admin) | Angemeldet |
|
||||||
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
||||||
| **Merkliste** | Tools als Favoriten speichern | Premium |
|
| **Merkliste** | Tools als Favoriten speichern | Premium |
|
||||||
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
||||||
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
||||||
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
||||||
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
||||||
|
| **Import** | Mehrere Tools per CSV, JSON oder YAML anlegen | Admin (Premium) |
|
||||||
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
||||||
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ tools**. Users maintain a shared catalog of tools, submit ratings
|
|||||||
| **Record costs** | Enter license and cost models per tool | Premium |
|
| **Record costs** | Enter license and cost models per tool | Premium |
|
||||||
| **Analytics** | Statistics, top tools, distributions | Everyone |
|
| **Analytics** | Statistics, top tools, distributions | Everyone |
|
||||||
| **Trash** | Restore soft-deleted tools or delete them permanently | Premium |
|
| **Trash** | Restore soft-deleted tools or delete them permanently | Premium |
|
||||||
|
| **Import** | Create multiple tools via CSV, JSON or YAML | Admin (premium) |
|
||||||
| **Admin** | User management, audit log, system information | Admin |
|
| **Admin** | User management, audit log, system information | Admin |
|
||||||
| **Redundancy detection** | Automatic duplicate detection | Admin |
|
| **Redundancy detection** | Automatic duplicate detection | Admin |
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Premium/Enterprise schalten folgende Features frei:
|
|||||||
| `watchlist` | Favoritenliste | [Merkliste](/docs/handbook/watchlist) |
|
| `watchlist` | Favoritenliste | [Merkliste](/docs/handbook/watchlist) |
|
||||||
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
||||||
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
||||||
|
| `tool-import` | Tools per CSV/JSON/YAML importieren (Admin) | [Administration](/docs/handbook/administration) |
|
||||||
|
|
||||||
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
||||||
Tarifverwaltung.
|
Tarifverwaltung.
|
||||||
@@ -26,6 +26,7 @@ Premium/Enterprise unlock the following features:
|
|||||||
| `watchlist` | Favorites list | [Watchlist](/docs/handbook/watchlist) |
|
| `watchlist` | Favorites list | [Watchlist](/docs/handbook/watchlist) |
|
||||||
| `trash` | Trash (soft-deleted tools) | [Trash](/docs/handbook/papierkorb) |
|
| `trash` | Trash (soft-deleted tools) | [Trash](/docs/handbook/papierkorb) |
|
||||||
| `costs` | Record cost/license models | [Recording costs](/docs/handbook/kosten) |
|
| `costs` | Record cost/license models | [Recording costs](/docs/handbook/kosten) |
|
||||||
|
| `tool-import` | Import tools via CSV/JSON/YAML (admin) | [Administration](/docs/handbook/administration) |
|
||||||
|
|
||||||
If you are missing a feature, the app shows an **upgrade notice** with a link
|
If you are missing a feature, the app shows an **upgrade notice** with a link
|
||||||
to the plan management.
|
to the plan management.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Mit **Filter zurücksetzen** bzw. **Alle entfernen** räumst du sie wieder auf.
|
|||||||
- **Ansicht wechseln:** Raster / Tabelle / Zeilen.
|
- **Ansicht wechseln:** Raster / Tabelle / Zeilen.
|
||||||
- **Dichte:** gemütlich / kompakt (Schieberegler).
|
- **Dichte:** gemütlich / kompakt (Schieberegler).
|
||||||
|
|
||||||
Deine Auswahl wird gespeichert — lokal im Browser und für angemeldete Nutzer:innen
|
Deine Auswahl wird gespeichert — lokal im Browser und für angemeldete Nutzer
|
||||||
zusätzlich serverseitig in den Präferenzen. Ansicht, Dichte, Suche, Filter und
|
zusätzlich serverseitig in den Präferenzen. Ansicht, Dichte, Suche, Filter und
|
||||||
Sortierung werden dabei in die URL übernommen, sodass du Ergebnisse teilen
|
Sortierung werden dabei in die URL übernommen, sodass du Ergebnisse teilen
|
||||||
kannst.
|
kannst.
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# v0.6.0 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-03 · **Tag:** [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- Complete modernization of all dependencies to the current major versions
|
|
||||||
(TypeScript 7, Vite 8, React 19.2, openid-client 6, zod 4, recharts 3, react-day-picker 10).
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- CI build fixed via `allowBuilds` configuration for pnpm 11
|
|
||||||
(build scripts for esbuild & Co. are no longer blocked).
|
|
||||||
- Image tagging simplified: only `latest` and `v*` tags, no more `nightly-*`/`sha-*` tags.
|
|
||||||
- All dependencies pinned exactly; automatic updates via Renovate prepared
|
|
||||||
(`renovate.json`, `docs/dependency-policy.md`).
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No breaking changes to the API. openid-client internally migrated to v6
|
|
||||||
(auth flow behaves identically).
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged. Node image pinned to `node:24.18.1-alpine`.
|
|
||||||
- **Migration:** no database migration required.
|
|
||||||
- **Breaking changes:** none.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- `typedoc` (indirect orval dependency) shows a peer-dependency warning
|
|
||||||
(expects TypeScript 5.x/6.x, 7.x is installed) — harmless for build and runtime.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`2f66fff`](https://git.kubebase.de/admin/tool-evaluator/commit/2f66fff)
|
|
||||||
- Tag: [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# v0.6.0 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-03 · **Tag:** [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- Vollständige Modernisierung aller Abhängigkeiten auf die aktuellen Hauptversionen
|
|
||||||
(TypeScript 7, Vite 8, React 19.2, openid-client 6, zod 4, recharts 3, react-day-picker 10).
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- CI-Build durch `allowBuilds`-Konfiguration für pnpm 11 repariert
|
|
||||||
(Build-Scripts für esbuild & Co. werden nicht mehr blockiert).
|
|
||||||
- Image-Tagging vereinfacht: nur noch `latest` und `v*`-Tags, keine `nightly-*`/`sha-*`-Tags.
|
|
||||||
- Alle Dependencies exakt gepinnt; automatische Updates via Renovate vorbereitet
|
|
||||||
(`renovate.json`, `docs/dependency-policy.md`).
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Breaking Changes an der API. openid-client intern auf v6 migriert
|
|
||||||
(auth-Fluss verhält sich identisch).
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert. Node-Image auf `node:24.18.1-alpine` gepinnt.
|
|
||||||
- **Migration:** keine Datenbank-Migration erforderlich.
|
|
||||||
- **Breaking Changes:** keine.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- `typedoc` (indirekte orval-Abhängigkeit) zeigt eine Peer-Dependency-Warnung
|
|
||||||
(erwartet TypeScript 5.x/6.x, installiert ist 7.x) — harmlos für Build & Laufzeit.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`2f66fff`](https://git.kubebase.de/admin/tool-evaluator/commit/2f66fff)
|
|
||||||
- Tag: [`v0.6.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.6.0)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# v0.7.0 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-03 · **Tag:** [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- Version-bound **release documentation** in the app under `/docs`
|
|
||||||
(index + detail page per version, Markdown from `docs/releases/`).
|
|
||||||
- Generated release template (`docs/releases/TEMPLATE.md`) and
|
|
||||||
sync step for the frontend build.
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- `tsx` bumped to 4.23.4 — last outdated dependency in the workspace
|
|
||||||
(`pnpm outdated -r` is now empty).
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No breaking changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- The documentation is currently limited to release notes; a complete
|
|
||||||
API/field reference will follow in v0.8.0.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`520f917`](https://git.kubebase.de/admin/tool-evaluator/commit/520f917)
|
|
||||||
- Tag: [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# v0.7.0 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-03 · **Tag:** [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- Version-gebundene **Release-Dokumentation** in der App unter `/docs`
|
|
||||||
(Index + Detailseite je Version, Markdown aus `docs/releases/`).
|
|
||||||
- Generiertes Release-Vorlage (`docs/releases/TEMPLATE.md`) und
|
|
||||||
Sync-Schritt für den Frontend-Build.
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- `tsx` auf 4.23.4 angehoben — letzte veraltete Abhängigkeit im Workspace
|
|
||||||
(`pnpm outdated -r` ist jetzt leer).
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Breaking Changes an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Die Doku ist bisher auf Release-Notes beschränkt; eine vollständige
|
|
||||||
API-/Feld-Referenz folgt in v0.8.0.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`520f917`](https://git.kubebase.de/admin/tool-evaluator/commit/520f917)
|
|
||||||
- Tag: [`v0.7.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.7.0)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# v0.8.0 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-03 · **Tag:** [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Complete documentation site** in mkdocs look under `/docs`:
|
|
||||||
- **Manual** with understandable explanations of all features
|
|
||||||
(Getting started, creating a tool, evaluations, comparing, watchlist,
|
|
||||||
analytics, administration, data model).
|
|
||||||
- **Automatically generated reference** from `lib/api-spec/openapi.yaml`:
|
|
||||||
all endpoints and data fields (type, required status, constraints) —
|
|
||||||
this guarantees that *every* feature is documented.
|
|
||||||
- **Search** across the manual, endpoints, and fields.
|
|
||||||
- **Version dropdown**: older releases keep their full field/endpoint
|
|
||||||
reference as a snapshot.
|
|
||||||
- **Repo link** to the source in the top right.
|
|
||||||
- **Help buttons (?) in forms** (NetBox style): next to each field,
|
|
||||||
an icon jumps directly to the field description in the docs.
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- Docs generator `scripts/src/generate-docs.mjs` replaces the previous
|
|
||||||
`sync-release-docs.mjs` (OpenAPI parsing, manual, search index, snapshots).
|
|
||||||
- Documentation for v0.7.0 backfilled.
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No breaking changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Manual & reference apply to the current version; older versions
|
|
||||||
show their release notes and a reference snapshot, if generated at
|
|
||||||
release time (`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`6c92b63`](https://git.kubebase.de/admin/tool-evaluator/commit/6c92b63)
|
|
||||||
- Tag: [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# v0.8.0 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-03 · **Tag:** [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- **Vollständige Dokumentations-Site** in mkdocs-Optik unter `/docs`:
|
|
||||||
- **Handbuch** mit verständlichen Erklärungen zu allen Features
|
|
||||||
(Erste Schritte, Tool anlegen, Bewertungen, Vergleichen, Merkliste,
|
|
||||||
Analytics, Administration, Datenmodell).
|
|
||||||
- **Automatisch generierte Referenz** aus `lib/api-spec/openapi.yaml`:
|
|
||||||
alle Endpunkte und Datenfelder (Typ, Pflichtstatus, Constraints) —
|
|
||||||
damit ist garantiert, dass *jedes* Feature dokumentiert ist.
|
|
||||||
- **Suche** über Handbuch, Endpunkte und Felder.
|
|
||||||
- **Versions-Dropdown**: ältere Releases behalten ihre vollständige
|
|
||||||
Feld-/Endpunkt-Referenz als Snapshot.
|
|
||||||
- **Repo-Link** oben rechts zur Quelle.
|
|
||||||
- **Hilfe-Buttons (?) in Formularen** (NetBox-Stil): neben jedem Feld
|
|
||||||
springt ein Icon direkt zur Feldbeschreibung in der Doku.
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- Doku-Generator `scripts/src/generate-docs.mjs` ersetzt den bisherigen
|
|
||||||
`sync-release-docs.mjs` (OpenAPI-Parsing, Handbuch, Suchindex, Snapshots).
|
|
||||||
- Dokumentation für v0.7.0 nachgezogen.
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Breaking Changes an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen
|
|
||||||
zeigen ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release
|
|
||||||
erzeugt (`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`6c92b63`](https://git.kubebase.de/admin/tool-evaluator/commit/6c92b63)
|
|
||||||
- Tag: [`v0.8.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.0)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# v0.8.1 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-03 · **Tag:** [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Standalone docs page**: The documentation is now available as its own,
|
|
||||||
mkdocs-like page at `toolr.kubebase.de/docs` — without the app shell
|
|
||||||
(own header with repo link, version dropdown, search, theme toggle
|
|
||||||
and link back to the app).
|
|
||||||
- **Navigation renamed**: The sidebar entry is now called **"Help"**
|
|
||||||
and leads to the standalone docs.
|
|
||||||
- **User guide completely revised**: 17 manual pages with
|
|
||||||
step-by-step instructions for all features (finding tools, creating a
|
|
||||||
tool, evaluating, watchlist, comparing, costs, analytics, plans,
|
|
||||||
administration, redundancy, trash, keyboard shortcuts, data model).
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- Reference links are now case-insensitive
|
|
||||||
(schema/endpoint slugs such as `toolinput` and `ToolInput` both work).
|
|
||||||
- Manual links to endpoint anchors corrected (PascalCase operation IDs).
|
|
||||||
- Outdated, broken manual links (`vergleichen`, `watchlist` …) replaced.
|
|
||||||
- Documentation table headers and notice texts in the docs page
|
|
||||||
internationalized via i18n (de/en).
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none. The docs page is reachable under `/docs` as
|
|
||||||
before; only the presentation is now standalone.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Manual & reference apply to the current version; older versions show
|
|
||||||
their release notes and a reference snapshot, if generated at release time
|
|
||||||
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`fdf2d74`](https://git.kubebase.de/admin/tool-evaluator/commit/fdf2d74)
|
|
||||||
- Tag: [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# v0.8.1 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-03 · **Tag:** [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- **Standalone-Doku-Seite**: Die Dokumentation steht jetzt als eigene,
|
|
||||||
mkdocs-artige Seite unter `toolr.kubebase.de/docs` — ohne die App-Shell
|
|
||||||
(eigene Kopfzeile mit Repo-Link, Versions-Dropdown, Suche, Theme-Umschalter
|
|
||||||
und Link zurück zur App).
|
|
||||||
- **Navigation umbenannt**: Der Seitenleisten-Eintrag heißt jetzt **„Hilfe"**
|
|
||||||
und führt zur Standalone-Doku.
|
|
||||||
- **User Guide komplett überarbeitet**: 17 Handbuch-Seiten mit
|
|
||||||
Schritt-für-Schritt-Anleitungen für alle Funktionen (Tools finden, Tool
|
|
||||||
anlegen, Bewerten, Merkliste, Vergleichen, Kosten, Analytics, Pläne,
|
|
||||||
Administration, Redundanz, Papierkorb, Tastenkürzel, Datenmodell).
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- Referenz-Links sind jetzt unabhängig von Groß-/Kleinschreibung
|
|
||||||
(Schema-/Endpoint-Slugs wie `toolinput` und `ToolInput` funktionieren beide).
|
|
||||||
- Handbuch-Links auf Endpunkt-Anker korrigiert (PascalCase-OperationIds).
|
|
||||||
- Veraltete, kaputte Handbuch-Links (`vergleichen`, `watchlist` …) ersetzt.
|
|
||||||
- Dokumentations-Tabellenkopfzeilen und Hinweistexte in der Doku-Seite über
|
|
||||||
i18n internationalisiert (de/en).
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Änderungen an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine. Die Doku-Seite ist unter `/docs` erreichbar wie
|
|
||||||
bisher; lediglich die Darstellung ist nun eigenständig.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen zeigen
|
|
||||||
ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release erzeugt
|
|
||||||
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`fdf2d74`](https://git.kubebase.de/admin/tool-evaluator/commit/fdf2d74)
|
|
||||||
- Tag: [`v0.8.1`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.1)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# v0.8.2 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-04 · **Tag:** [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- **Mobile navigation in the docs**: The left navigation bar (manual,
|
|
||||||
endpoints, schemas) was completely hidden below `lg` (1024px).
|
|
||||||
There is now a menu button in the header that opens a side
|
|
||||||
navigation drawer with the same content — on small screens
|
|
||||||
the docs remain fully navigable.
|
|
||||||
- **Version dropdown shows the selected version**: When switching to a
|
|
||||||
different release (`/docs/releases/vX.Y.Z`), the dropdown wrongly stayed on
|
|
||||||
"current". The displayed version is now also derived from the releases route.
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Manual & reference apply to the current version; older versions show
|
|
||||||
their release notes and a reference snapshot, if generated at release time
|
|
||||||
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`33cf261`](https://git.kubebase.de/admin/tool-evaluator/commit/33cf261)
|
|
||||||
- Tag: [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# v0.8.2 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-04 · **Tag:** [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- **Mobile Navigation in der Doku**: Die linke Navigationsleiste (Handbuch,
|
|
||||||
Endpunkte, Schemas) wurde unterhalb von `lg` (1024px) komplett ausgeblendet.
|
|
||||||
Es gibt jetzt einen Menü-Button im Kopfbereich, der ein seitliches
|
|
||||||
Navigations-Drawer mit demselben Inhalt öffnet — auf kleinen Bildschirmen
|
|
||||||
bleibt die Doku voll navigierbar.
|
|
||||||
- **Versions-Dropdown zeigt die gewählte Version an**: Beim Wechsel zu einem
|
|
||||||
anderen Release (`/docs/releases/vX.Y.Z`) blieb das Dropdown fälschlich auf
|
|
||||||
„aktuell". Die angezeigte Version wird jetzt auch aus der releases-Route
|
|
||||||
abgeleitet.
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Änderungen an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Handbuch & Referenz gelten für die aktuelle Version; ältere Versionen zeigen
|
|
||||||
ihre Release-Notes und einen Referenz-Snapshot, sofern beim Release erzeugt
|
|
||||||
(`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`).
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`33cf261`](https://git.kubebase.de/admin/tool-evaluator/commit/33cf261)
|
|
||||||
- Tag: [`v0.8.2`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.2)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# v0.8.3 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-04 · **Tag:** [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Version-based documentation**: Each release now carries a
|
|
||||||
complete docs snapshot (manual + API reference). Via the
|
|
||||||
version dropdown you can reach the docs of the respective version
|
|
||||||
(`/docs/vX.Y.Z/…`) — including the manual and reference as they
|
|
||||||
applied at release time. The last 7 versions remain available.
|
|
||||||
- **Help buttons in forms (NetBox style)**: For evaluation, tool
|
|
||||||
create/edit, costs, comparing, watchlist and analytics there is now
|
|
||||||
a help icon that leads directly to the matching guide in the manual.
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- **Gitea tag links fixed**: `…/tags/vX.Y.Z` was a 404 page; the
|
|
||||||
correct URL is `/releases/tag/vX.Y.Z` (in all release notes and the
|
|
||||||
docs page).
|
|
||||||
- Version switching now leads to the docs of the selected version instead of
|
|
||||||
just the release page; old `/docs/vX.Y.Z` redirects are removed.
|
|
||||||
- Docs generator: `--snapshot` creates manual + reference per version; on
|
|
||||||
build, the snapshots of the last 7 versions are included.
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none. New release notes must use the correct
|
|
||||||
tag link (see `docs/releases/TEMPLATE.md`). For versions before
|
|
||||||
this release, snapshots can be created retroactively:
|
|
||||||
`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Older releases without a snapshot (e.g. v0.6.0) only show their release notes,
|
|
||||||
no manual/reference. Backfill with `--snapshot`.
|
|
||||||
- The docs version corresponds to the state of the docs at the time the
|
|
||||||
snapshot was created; manual changes apply from the respective next release.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`3bb4598`](https://git.kubebase.de/admin/tool-evaluator/commit/3bb4598)
|
|
||||||
- Tag: [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# v0.8.3 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-04 · **Tag:** [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- **Versionsbasierte Dokumentation**: Jedes Release trägt jetzt einen
|
|
||||||
vollständigen Doku-Snapshot (Handbuch + API-Referenz). Über das
|
|
||||||
Versions-Dropdown gelangst du zur Doku der jeweiligen Version
|
|
||||||
(`/docs/vX.Y.Z/…`) — inklusive Handbuch und Referenz, wie sie zum
|
|
||||||
Release-Zeitpunkt galten. Die letzten 7 Versionen bleiben verfügbar.
|
|
||||||
- **Hilfe-Buttons in Formularen (NetBox-Stil)**: Bei Bewertung, Tool
|
|
||||||
anlegen/bearbeiten, Kosten, Vergleichen, Merkliste und Analytics gibt es
|
|
||||||
jetzt ein Hilfe-Icon, das direkt zur passenden Anleitung im Handbuch führt.
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- **Gitea-Tag-Links korrigiert**: `…/tags/vX.Y.Z` war eine 404-Seite; die
|
|
||||||
korrekte URL lautet `/releases/tag/vX.Y.Z` (in allen Release-Notes und der
|
|
||||||
Doku-Seite).
|
|
||||||
- Versions-Wechsel führt jetzt zur Doku der gewählten Version statt direkt
|
|
||||||
nur zur Release-Seite; alte `/docs/vX.Y.Z`-Redirects entfallen.
|
|
||||||
- Doku-Generator: `--snapshot` erzeugt Handbuch + Referenz pro Version; beim
|
|
||||||
Build werden die Snapshots der letzten 7 Versionen eingespielt.
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Änderungen an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine. Neue Release-Notes müssen den korrekten
|
|
||||||
Tag-Link verwenden (siehe `docs/releases/TEMPLATE.md`). Für Versionen vor
|
|
||||||
diesem Release können Snapshots nachträglich erzeugt werden:
|
|
||||||
`node scripts/src/generate-docs.mjs --snapshot vX.Y.Z`.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Ältere Releases ohne Snapshot (z. B. v0.6.0) zeigen nur ihre Release-Notes,
|
|
||||||
kein Handbuch/keine Referenz. Mit `--snapshot` nachpflegen.
|
|
||||||
- Die Doku-Version entspricht dem Doku-Stand zum Zeitpunkt der Erstellung des
|
|
||||||
Snapshots; Änderungen am Handbuch gelten ab dem jeweils nächsten Release.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`3bb4598`](https://git.kubebase.de/admin/tool-evaluator/commit/3bb4598)
|
|
||||||
- Tag: [`v0.8.3`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.3)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# v0.8.4 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-04 · **Tag:** [`v0.8.4`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.4)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Full app localization (German/English)**: All pages and components are
|
|
||||||
now translated — tool detail page, admin area, trash, redundancy dashboard,
|
|
||||||
compare, watchlist, analytics, create and edit tool, admin tools tab,
|
|
||||||
category combobox and theme toggle. The language picker in the top right
|
|
||||||
switches the whole UI.
|
|
||||||
- **Bilingual documentation**: The manual (all 17 pages) and the release
|
|
||||||
notes are now available in German and English. The docs page has a
|
|
||||||
language switcher that switches content, navigation titles and search.
|
|
||||||
Versioned docs snapshots (v0.8.1, v0.8.2) also contain the English pages.
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- All remaining hardcoded UI strings replaced with `t()` calls; the locales
|
|
||||||
are in sync (453 keys per language, 0 missing).
|
|
||||||
- API reference: table headers (Name/In), required badges and request body
|
|
||||||
labels in the docs are translated.
|
|
||||||
- Docs search respects the active language (separate index per language).
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none. The docs generator output now includes
|
|
||||||
additional English files (`*.en.md`) and `search.en.json`; these are
|
|
||||||
generated automatically during the build.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- Versioned docs snapshots before v0.8.1 (e.g. v0.6.0) do not contain
|
|
||||||
English manual pages; the German version is used as fallback there.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`3de59cc`](https://git.kubebase.de/admin/tool-evaluator/commit/3de59cc)
|
|
||||||
- Tag: [`v0.8.4`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.4)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# v0.8.4 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-04 · **Tag:** [`v0.8.4`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.4)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- **Komplette Lokalisierung der App (Deutsch/Englisch)**: Alle Seiten und
|
|
||||||
Komponenten sind jetzt übersetzt — Tool-Detailseite, Verwaltung, Papierkorb,
|
|
||||||
Redundanz-Dashboard, Vergleichen, Merkliste, Analytics, Tool anlegen und
|
|
||||||
bearbeiten, Admin-Bereich inkl. Tools-Tab, Kategorie-Combobox und
|
|
||||||
Theme-Umschalter. Die Sprachauswahl oben rechts wechselt die gesamte
|
|
||||||
Oberfläche.
|
|
||||||
- **Dokumentation zweisprachig**: Handbuch (alle 17 Seiten) und Release-Notes
|
|
||||||
sind jetzt in Deutsch und Englisch verfügbar. In der Doku gibt es einen
|
|
||||||
Sprach-Umschalter, der Inhalte, Navigationstitel und Suche umschaltet.
|
|
||||||
Auch versionierte Doku-Snapshots (v0.8.1, v0.8.2) enthalten die englischen
|
|
||||||
Seiten.
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- Alle verbliebenen hartkodierten UI-Strings durch `t()`-Aufrufe ersetzt;
|
|
||||||
die Locales sind synchron (453 Keys pro Sprache, 0 fehlend).
|
|
||||||
- API-Referenz: Tabellen-Header (Name/In), Pflicht-Badges und
|
|
||||||
Request-Body-Label der Doku übersetzt.
|
|
||||||
- Suche in der Doku berücksichtigt die aktive Sprache (eigener Index pro
|
|
||||||
Sprache).
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Änderungen an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine. Die Doku-Generator-Ausgabe umfasst jetzt
|
|
||||||
zusätzliche englische Dateien (`*.en.md`) und `search.en.json`; beim Build
|
|
||||||
werden diese automatisch erzeugt.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Versionierte Doku-Snapshots vor v0.8.1 (z. B. v0.6.0) enthalten keine
|
|
||||||
englischen Handbuch-Seiten; dort greift die deutsche Fassung als Fallback.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`3de59cc`](https://git.kubebase.de/admin/tool-evaluator/commit/3de59cc)
|
|
||||||
- Tag: [`v0.8.4`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.4)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# v0.8.5 — Release Notes
|
|
||||||
|
|
||||||
**Date:** 2026-08-04 · **Tag:** [`v0.8.5`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.5)
|
|
||||||
|
|
||||||
## New features
|
|
||||||
|
|
||||||
- **Help buttons across the app**: The main pages (Home, Browse tools, Trash,
|
|
||||||
Redundancy dashboard, Admin tools) as well as the premium dialog and the
|
|
||||||
password dialog now show a book icon next to the title that links directly
|
|
||||||
to the matching manual page (following the existing buttons on the tool
|
|
||||||
detail, create, edit, compare, watchlist and analytics pages).
|
|
||||||
|
|
||||||
## Fixes & improvements
|
|
||||||
|
|
||||||
- **Translated submit button when creating a tool**: The "Submit Tool" /
|
|
||||||
"Adding Tool…" button was still hardcoded and appeared in English in the
|
|
||||||
German UI. It is now translated, e.g. "Tool einreichen" / "Füge Tool
|
|
||||||
hinzu…" in German.
|
|
||||||
- **More translated UI strings**: View mode (Grid/Table/Rows), list density
|
|
||||||
(Compact/Comfortable), minimum rating in the filter and the password hint
|
|
||||||
"min. 6 characters" are now translated correctly.
|
|
||||||
- **Consistent docs terminology**: The manual now consistently calls the
|
|
||||||
watchlist "Merkliste" in all German pages and release notes; the English
|
|
||||||
version stays "Watchlist". The `watchlist` permission name and the URLs
|
|
||||||
are unchanged.
|
|
||||||
- All locales are in sync (462 keys per language, 0 missing).
|
|
||||||
|
|
||||||
## API changes
|
|
||||||
|
|
||||||
- No changes to the API.
|
|
||||||
|
|
||||||
## Operations / upgrade
|
|
||||||
|
|
||||||
- **Env vars:** unchanged.
|
|
||||||
- **Migration:** none.
|
|
||||||
- **Breaking changes:** none.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- None.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`70e72da`](https://git.kubebase.de/admin/tool-evaluator/commit/70e72da)
|
|
||||||
- Tag: [`v0.8.5`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.5)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# v0.8.5 — Release Notes
|
|
||||||
|
|
||||||
**Datum:** 2026-08-04 · **Tag:** [`v0.8.5`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.5)
|
|
||||||
|
|
||||||
## Neue Features
|
|
||||||
|
|
||||||
- **Hilfe-Buttons in der gesamten App**: Auf den Hauptseiten (Start, Tools
|
|
||||||
durchsuchen, Papierkorb, Redundanz-Dashboard, Admin-Tools) sowie in
|
|
||||||
Premium-Dialog und Passwort-Dialog gibt es jetzt ein Buch-Icon neben dem
|
|
||||||
Titel, das direkt zur passenden Anleitung im Handbuch verlinkt (analog zu
|
|
||||||
den bestehenden Buttons auf Tool-Detail-, Anlegen-, Bearbeiten-,
|
|
||||||
Vergleichen-, Merkliste- und Analytics-Seiten).
|
|
||||||
|
|
||||||
## Fixes & Verbesserungen
|
|
||||||
|
|
||||||
- **Submit-Button beim Tool-Anlegen übersetzt**: Der Button „Submit Tool" /
|
|
||||||
„Adding Tool…" war noch hartkodiert und erschien auf Deutsch auf Englisch.
|
|
||||||
Jetzt: „Tool einreichen" / „Füge Tool hinzu…" (bzw. die englischen
|
|
||||||
Entsprechungen).
|
|
||||||
- **Weitere übersetzte UI-Strings**: Ansichtsmodus (Raster/Tabelle/Zeilen),
|
|
||||||
Listendichte (Kompakt/Komfortabel), Mindestbewertung im Filter sowie der
|
|
||||||
Passwort-Hinweis „min. 6 Zeichen" werden jetzt korrekt übersetzt.
|
|
||||||
- **Doku-Terminologie konsistent**: Das Handbuch heißt die Merkliste jetzt
|
|
||||||
einheitlich „Merkliste" (statt „Watchlist") in allen deutschen Seiten und
|
|
||||||
Release-Notes; die englische Fassung bleibt „Watchlist". Die
|
|
||||||
Permissions-Bezeichnung `watchlist` und die URLs sind unverändert.
|
|
||||||
- Alle Locales synchron (462 Keys pro Sprache, 0 fehlend).
|
|
||||||
|
|
||||||
## API-Änderungen
|
|
||||||
|
|
||||||
- Keine Änderungen an der API.
|
|
||||||
|
|
||||||
## Betrieb / Upgrade
|
|
||||||
|
|
||||||
- **Env-Vars:** unverändert.
|
|
||||||
- **Migration:** keine.
|
|
||||||
- **Breaking Changes:** keine.
|
|
||||||
|
|
||||||
## Bekannte Einschränkungen
|
|
||||||
|
|
||||||
- Keine.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
- Commit: [`70e72da`](https://git.kubebase.de/admin/tool-evaluator/commit/70e72da)
|
|
||||||
- Tag: [`v0.8.5`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.8.5)
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# v0.9.0 — Release Notes
|
||||||
|
|
||||||
|
**Datum:** 2026-08-05 · **Tag:** [`v0.9.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.9.0)
|
||||||
|
|
||||||
|
## Neue Features
|
||||||
|
|
||||||
|
- **Neuausrichtung der Versionierung:** Die Dokumentation startet mit dieser
|
||||||
|
Version neu bei `v0.9.0`. Ältere Release-Notizen und Versionen-Snapshots
|
||||||
|
(v0.6.0–v0.8.6) wurden entfernt. Das Handbuch und die Referenz (API/Admin)
|
||||||
|
sind jetzt wieder für jede Version verfügbar — auch für neuere Versionen.
|
||||||
|
- **Sprachsuffixe:** Alle Dokumentationsdateien verwenden jetzt einheitlich die
|
||||||
|
Suffixe `.de.md` und `.en.md`. Damit lassen sich später weitere Sprachen
|
||||||
|
ergänzen; die App liefert automatisch die passende Sprachfassung.
|
||||||
|
- **Import — fertige Beispieldatei:** Unter `docs/examples/import-tools.yaml`
|
||||||
|
liegt eine sofort importierbare YAML-Beispieldatei mit 24 gängigen
|
||||||
|
Enterprise-Tools (Kategorien wie Kommunikation, Monitoring, CI/CD,
|
||||||
|
Infrastruktur). Die Datei ist in der Administration-Dokumentation verlinkt.
|
||||||
|
|
||||||
|
## Fixes & Verbesserungen
|
||||||
|
|
||||||
|
- Der Versionsumschalter zeigt die Dokumentation jetzt auch für neuere
|
||||||
|
Versionen korrekt an (vorher fehlende Versionen-Snapshots machten Handbuch und
|
||||||
|
Referenz nicht verfügbar).
|
||||||
|
- Wording bereinigt: Die Tagline und Platzhaltertexte ohne Berufsbezeichnung
|
||||||
|
(„Bewerte dein Tool." / „Rate your Tool.", „Anonymer Benutzer" /
|
||||||
|
„Anonymous User"). Es werden keine Gender-Sonderformen mehr verwendet
|
||||||
|
(`Nutzer:innen` → `Nutzer`, `Ersteller:in` → `Ersteller`).
|
||||||
|
|
||||||
|
## API-Änderungen
|
||||||
|
|
||||||
|
- Keine Änderungen gegenüber v0.8.6 (siehe `lib/api-spec/openapi.yaml`).
|
||||||
|
|
||||||
|
## Betrieb / Upgrade
|
||||||
|
|
||||||
|
- **Migration:** keine. Bereits vorhandene Tools-, Bewertungs- und Konto-Daten
|
||||||
|
bleiben unverändert.
|
||||||
|
- **Breaking Changes:** keine.
|
||||||
|
|
||||||
|
## Bekannte Einschränkungen
|
||||||
|
|
||||||
|
- Keine im Rahmen dieses Releases.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`6eb3dfd`](https://git.kubebase.de/admin/tool-evaluator/commit/6eb3dfd)
|
||||||
|
- Tag: [`v0.9.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.9.0)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# v0.9.0 — Release Notes
|
||||||
|
|
||||||
|
**Date:** 2026-08-05 · **Tag:** [`v0.9.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.9.0)
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
- **Versioning reset:** Documentation starts fresh at `v0.9.0` with this
|
||||||
|
release. Older release notes and version snapshots (v0.6.0–v0.8.6) have been
|
||||||
|
removed. The handbook and reference (API/admin) are now available again for
|
||||||
|
every version — including newer versions.
|
||||||
|
- **Language suffixes:** All documentation files now consistently use the
|
||||||
|
`.de.md` and `.en.md` suffixes. This makes it easy to add more languages
|
||||||
|
later; the app automatically serves the matching language variant.
|
||||||
|
- **Import — ready-to-use example file:** A ready-to-import YAML example file
|
||||||
|
with 24 common enterprise tools is provided at
|
||||||
|
`docs/examples/import-tools.yaml` (categories such as communication,
|
||||||
|
monitoring, CI/CD, infrastructure). The file is linked from the
|
||||||
|
administration documentation.
|
||||||
|
|
||||||
|
## Fixes & Polish
|
||||||
|
|
||||||
|
- The version switcher now displays documentation correctly for newer versions
|
||||||
|
as well (previously, missing version snapshots made the handbook and
|
||||||
|
reference unavailable).
|
||||||
|
- Wording cleanup: tagline and placeholder texts no longer reference a specific
|
||||||
|
job title ("Rate your Tool.", "Anonymous User"). No gender-specific forms are
|
||||||
|
used anymore.
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
- No changes compared to v0.8.6 (see `lib/api-spec/openapi.yaml`).
|
||||||
|
|
||||||
|
## Operations / Upgrade
|
||||||
|
|
||||||
|
- **Migration:** none. Existing tool, rating, and account data remain
|
||||||
|
unchanged.
|
||||||
|
- **Breaking Changes:** none.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- None within the scope of this release.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Commit: [`6eb3dfd`](https://git.kubebase.de/admin/tool-evaluator/commit/6eb3dfd)
|
||||||
|
- Tag: [`v0.9.0`](https://git.kubebase.de/admin/tool-evaluator/releases/tag/v0.9.0)
|
||||||
+43
@@ -32,6 +32,49 @@ Zentraler Zugriff auf den Tool-Katalog.
|
|||||||
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
- **Massenaktion:** mehrere Tools auswählen und in den Papierkorb verschieben
|
||||||
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
(Bestätigungsdialog; soft gelöschte Tools sind aus allen öffentlichen Ansichten
|
||||||
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
entfernt und können wiederhergestellt oder endgültig gelöscht werden).
|
||||||
|
- **Importieren** (Premium): über die Schaltfläche **Importieren** oben rechts
|
||||||
|
können mehrere Tools gleichzeitig angelegt werden. Das Format wird automatisch
|
||||||
|
erkannt (CSV, JSON oder YAML); für CSV lässt sich der Trenner wählen
|
||||||
|
(automatisch, Komma, Semikolon oder Tabulator). Die Daten lassen sich direkt
|
||||||
|
ins Formular einfügen oder als Datei hochladen.
|
||||||
|
|
||||||
|
Unterstützte Felder: `name` (Pflicht), `description`, `category`, `websiteUrl`,
|
||||||
|
`iconUrl`, `features`, `tags` (`features`/`tags` sind mit `|` getrennte Listen).
|
||||||
|
|
||||||
|
**CSV-Beispiel:**
|
||||||
|
|
||||||
|
```csv
|
||||||
|
name;description;category;features;tags
|
||||||
|
Grafana;Observability- und Dashboard-Plattform;Monitoring;Dashboards|Alerts;devops
|
||||||
|
Prometheus;Zeitreihen-Datenbank;Monitoring;Metrics;devops
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON-Beispiel:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "Grafana", "category": "Monitoring", "features": ["Dashboards"] },
|
||||||
|
{ "name": "Prometheus", "category": "Monitoring" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**YAML-Beispiel:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Grafana
|
||||||
|
category: Monitoring
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- name: Prometheus
|
||||||
|
category: Monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
Eine umfangreichere, fertig importierbare Beispieldatei liegt unter
|
||||||
|
`docs/examples/import-tools.yaml` (24 gängige Enterprise-Tools).
|
||||||
|
|
||||||
|
Beim Import werden einzelne fehlerhafte Zeilen übersprungen und mit Zeilennummer
|
||||||
|
aufgelistet; korrekte Zeilen werden trotzdem angelegt. Jeder Import erzeugt
|
||||||
|
Audit-Log-Einträge.
|
||||||
|
|
||||||
## Tab „Audit-Log"
|
## Tab „Audit-Log"
|
||||||
|
|
||||||
+43
@@ -32,6 +32,49 @@ Central access to the tool catalog.
|
|||||||
- **Bulk action:** select multiple tools and move them to the trash
|
- **Bulk action:** select multiple tools and move them to the trash
|
||||||
(confirmation dialog; soft-deleted tools are removed from all public views
|
(confirmation dialog; soft-deleted tools are removed from all public views
|
||||||
and can be restored or permanently deleted).
|
and can be restored or permanently deleted).
|
||||||
|
- **Import** (premium): via the **Import** button at the top right, multiple
|
||||||
|
tools can be created at once. The format is detected automatically (CSV,
|
||||||
|
JSON or YAML); for CSV the delimiter can be chosen (auto-detect, comma,
|
||||||
|
semicolon or tab). Data can be pasted directly into the form or uploaded
|
||||||
|
as a file.
|
||||||
|
|
||||||
|
Supported fields: `name` (required), `description`, `category`, `websiteUrl`,
|
||||||
|
`iconUrl`, `features`, `tags` (`features`/`tags` are `|`-separated lists).
|
||||||
|
|
||||||
|
**CSV example:**
|
||||||
|
|
||||||
|
```csv
|
||||||
|
name;description;category;features;tags
|
||||||
|
Grafana;Observability and dashboard platform;Monitoring;Dashboards|Alerts;devops
|
||||||
|
Prometheus;Time-series database;Monitoring;Metrics;devops
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{ "name": "Grafana", "category": "Monitoring", "features": ["Dashboards"] },
|
||||||
|
{ "name": "Prometheus", "category": "Monitoring" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**YAML example:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Grafana
|
||||||
|
category: Monitoring
|
||||||
|
features:
|
||||||
|
- Dashboards
|
||||||
|
- name: Prometheus
|
||||||
|
category: Monitoring
|
||||||
|
```
|
||||||
|
|
||||||
|
A larger, ready-to-import example file is located at
|
||||||
|
`docs/examples/import-tools.yaml` (24 common enterprise tools).
|
||||||
|
|
||||||
|
During import, individual faulty rows are skipped and listed with their row
|
||||||
|
number; correct rows are still created. Every import produces audit-log
|
||||||
|
entries.
|
||||||
|
|
||||||
## "Audit log" tab
|
## "Audit log" tab
|
||||||
|
|
||||||
+2
-2
@@ -6,7 +6,7 @@ Besuch bis zum Anlegen und Bewerten eines Tools.
|
|||||||
|
|
||||||
## 1. Anmelden
|
## 1. Anmelden
|
||||||
|
|
||||||
Die meisten Aktionen (Tool anlegen, bewerten, Watchlist, Vergleichen) erfordern
|
Die meisten Aktionen (Tool anlegen, bewerten, Merkliste, Vergleichen) erfordern
|
||||||
ein Konto. Klicke unten links auf **Anmelden**. Je nach Konfiguration der
|
ein Konto. Klicke unten links auf **Anmelden**. Je nach Konfiguration der
|
||||||
Instanz hast du zwei Möglichkeiten:
|
Instanz hast du zwei Möglichkeiten:
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ Siehe [Bewerten](/docs/handbook/bewerten).
|
|||||||
## 5. Weiterführend
|
## 5. Weiterführend
|
||||||
|
|
||||||
- [Tools vergleichen](/docs/handbook/vergleichen)
|
- [Tools vergleichen](/docs/handbook/vergleichen)
|
||||||
- [Watchlist](/docs/handbook/watchlist)
|
- [Merkliste](/docs/handbook/watchlist)
|
||||||
- [Analytics](/docs/handbook/analytics)
|
- [Analytics](/docs/handbook/analytics)
|
||||||
- [Pläne & Berechtigungen](/docs/handbook/plaene)
|
- [Pläne & Berechtigungen](/docs/handbook/plaene)
|
||||||
- [Administration](/docs/handbook/administration)
|
- [Administration](/docs/handbook/administration)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# Willkommen bei toolr
|
# Willkommen bei toolr
|
||||||
|
|
||||||
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
toolr ist eine Plattform zum **Entdecken, Bewerten und Vergleichen von
|
||||||
Entwicklungstools**. Nutzer:innen pflegen einen gemeinsamen Katalog von Tools,
|
Entwicklungstools**. Nutzer pflegen einen gemeinsamen Katalog von Tools,
|
||||||
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
vergeben Bewertungen (Nützlichkeit & Bedienbarkeit) und nutzen Statistiken, um
|
||||||
die richtige Wahl zu treffen.
|
die richtige Wahl zu treffen.
|
||||||
|
|
||||||
@@ -12,13 +12,14 @@ die richtige Wahl zu treffen.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
| **Tools durchsuchen** | Katalog filtern, sortieren und durchsuchen | Alle |
|
||||||
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
| **Tool anlegen** | Neues Tool mit Beschreibung, Kategorie, Features & Tags eintragen | Angemeldet |
|
||||||
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller:in oder Admin) | Angemeldet |
|
| **Tool bearbeiten/löschen** | Eigene Tools pflegen (Ersteller oder Admin) | Angemeldet |
|
||||||
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
| **Bewerten** | Nützlichkeit & Bedienbarkeit (1–5) plus Kommentar vergeben | Angemeldet |
|
||||||
| **Watchlist** | Tools als Favoriten speichern | Premium |
|
| **Merkliste** | Tools als Favoriten speichern | Premium |
|
||||||
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
| **Vergleichen** | Tools nebeneinander gegenüberstellen | Premium |
|
||||||
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
| **Kosten erfassen** | Lizenz- und Kostenmodelle je Tool eintragen | Premium |
|
||||||
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
| **Analytics** | Statistiken, Top-Tools, Verteilungen | Alle |
|
||||||
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
| **Papierkorb** | Soft-gelöschte Tools wiederherstellen oder endgültig löschen | Premium |
|
||||||
|
| **Import** | Mehrere Tools per CSV, JSON oder YAML anlegen | Admin (Premium) |
|
||||||
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
| **Admin** | Nutzerverwaltung, Audit-Log, Systeminformationen | Admin |
|
||||||
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
| **Redundanz** | Automatische Doppelungs-Erkennung | Admin |
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ tools**. Users maintain a shared catalog of tools, submit ratings
|
|||||||
| **Record costs** | Enter license and cost models per tool | Premium |
|
| **Record costs** | Enter license and cost models per tool | Premium |
|
||||||
| **Analytics** | Statistics, top tools, distributions | Everyone |
|
| **Analytics** | Statistics, top tools, distributions | Everyone |
|
||||||
| **Trash** | Restore soft-deleted tools or delete them permanently | Premium |
|
| **Trash** | Restore soft-deleted tools or delete them permanently | Premium |
|
||||||
|
| **Import** | Create multiple tools via CSV, JSON or YAML | Admin (premium) |
|
||||||
| **Admin** | User management, audit log, system information | Admin |
|
| **Admin** | User management, audit log, system information | Admin |
|
||||||
| **Redundancy detection** | Automatic duplicate detection | Admin |
|
| **Redundancy detection** | Automatic duplicate detection | Admin |
|
||||||
|
|
||||||
+18
-18
@@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"slug": "index",
|
"slug": "index",
|
||||||
"file": "index.md",
|
"file": "index.de.md",
|
||||||
"title": "Überblick",
|
"title": "Überblick",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"fileEn": "index.en.md",
|
"fileEn": "index.en.md",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "getting-started",
|
"slug": "getting-started",
|
||||||
"file": "getting-started.md",
|
"file": "getting-started.de.md",
|
||||||
"title": "Erste Schritte",
|
"title": "Erste Schritte",
|
||||||
"order": 2,
|
"order": 2,
|
||||||
"fileEn": "getting-started.en.md",
|
"fileEn": "getting-started.en.md",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "konto",
|
"slug": "konto",
|
||||||
"file": "konto.md",
|
"file": "konto.de.md",
|
||||||
"title": "Anmelden & Konto",
|
"title": "Anmelden & Konto",
|
||||||
"order": 3,
|
"order": 3,
|
||||||
"fileEn": "konto.en.md",
|
"fileEn": "konto.en.md",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "tools-finden",
|
"slug": "tools-finden",
|
||||||
"file": "tools-finden.md",
|
"file": "tools-finden.de.md",
|
||||||
"title": "Tools finden & durchsuchen",
|
"title": "Tools finden & durchsuchen",
|
||||||
"order": 4,
|
"order": 4,
|
||||||
"fileEn": "tools-finden.en.md",
|
"fileEn": "tools-finden.en.md",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "tool-anlegen",
|
"slug": "tool-anlegen",
|
||||||
"file": "tool-anlegen.md",
|
"file": "tool-anlegen.de.md",
|
||||||
"title": "Tool anlegen",
|
"title": "Tool anlegen",
|
||||||
"order": 5,
|
"order": 5,
|
||||||
"fileEn": "tool-anlegen.en.md",
|
"fileEn": "tool-anlegen.en.md",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "tool-bearbeiten",
|
"slug": "tool-bearbeiten",
|
||||||
"file": "tool-bearbeiten.md",
|
"file": "tool-bearbeiten.de.md",
|
||||||
"title": "Tool bearbeiten & löschen",
|
"title": "Tool bearbeiten & löschen",
|
||||||
"order": 6,
|
"order": 6,
|
||||||
"fileEn": "tool-bearbeiten.en.md",
|
"fileEn": "tool-bearbeiten.en.md",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "bewerten",
|
"slug": "bewerten",
|
||||||
"file": "bewerten.md",
|
"file": "bewerten.de.md",
|
||||||
"title": "Bewerten",
|
"title": "Bewerten",
|
||||||
"order": 7,
|
"order": 7,
|
||||||
"fileEn": "bewerten.en.md",
|
"fileEn": "bewerten.en.md",
|
||||||
@@ -57,15 +57,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "watchlist",
|
"slug": "watchlist",
|
||||||
"file": "watchlist.md",
|
"file": "watchlist.de.md",
|
||||||
"title": "Watchlist",
|
"title": "Merkliste",
|
||||||
"order": 8,
|
"order": 8,
|
||||||
"fileEn": "watchlist.en.md",
|
"fileEn": "watchlist.en.md",
|
||||||
"titleEn": "Watchlist"
|
"titleEn": "Watchlist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "vergleichen",
|
"slug": "vergleichen",
|
||||||
"file": "vergleichen.md",
|
"file": "vergleichen.de.md",
|
||||||
"title": "Vergleichen",
|
"title": "Vergleichen",
|
||||||
"order": 9,
|
"order": 9,
|
||||||
"fileEn": "vergleichen.en.md",
|
"fileEn": "vergleichen.en.md",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "analytics",
|
"slug": "analytics",
|
||||||
"file": "analytics.md",
|
"file": "analytics.de.md",
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"order": 10,
|
"order": 10,
|
||||||
"fileEn": "analytics.en.md",
|
"fileEn": "analytics.en.md",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "plaene",
|
"slug": "plaene",
|
||||||
"file": "plaene.md",
|
"file": "plaene.de.md",
|
||||||
"title": "Pläne & Berechtigungen",
|
"title": "Pläne & Berechtigungen",
|
||||||
"order": 11,
|
"order": 11,
|
||||||
"fileEn": "plaene.en.md",
|
"fileEn": "plaene.en.md",
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "kosten",
|
"slug": "kosten",
|
||||||
"file": "kosten.md",
|
"file": "kosten.de.md",
|
||||||
"title": "Kosten erfassen",
|
"title": "Kosten erfassen",
|
||||||
"order": 12,
|
"order": 12,
|
||||||
"fileEn": "kosten.en.md",
|
"fileEn": "kosten.en.md",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "administration",
|
"slug": "administration",
|
||||||
"file": "administration.md",
|
"file": "administration.de.md",
|
||||||
"title": "Administration",
|
"title": "Administration",
|
||||||
"order": 13,
|
"order": 13,
|
||||||
"fileEn": "administration.en.md",
|
"fileEn": "administration.en.md",
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "redundanz",
|
"slug": "redundanz",
|
||||||
"file": "redundanz.md",
|
"file": "redundanz.de.md",
|
||||||
"title": "Redundanz-Dashboard",
|
"title": "Redundanz-Dashboard",
|
||||||
"order": 14,
|
"order": 14,
|
||||||
"fileEn": "redundanz.en.md",
|
"fileEn": "redundanz.en.md",
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "papierkorb",
|
"slug": "papierkorb",
|
||||||
"file": "papierkorb.md",
|
"file": "papierkorb.de.md",
|
||||||
"title": "Papierkorb",
|
"title": "Papierkorb",
|
||||||
"order": 15,
|
"order": 15,
|
||||||
"fileEn": "papierkorb.en.md",
|
"fileEn": "papierkorb.en.md",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "tastatur",
|
"slug": "tastatur",
|
||||||
"file": "tastatur.md",
|
"file": "tastatur.de.md",
|
||||||
"title": "Tastenkürzel & Kommandopalette",
|
"title": "Tastenkürzel & Kommandopalette",
|
||||||
"order": 16,
|
"order": 16,
|
||||||
"fileEn": "tastatur.en.md",
|
"fileEn": "tastatur.en.md",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "datenmodell",
|
"slug": "datenmodell",
|
||||||
"file": "datenmodell.md",
|
"file": "datenmodell.de.md",
|
||||||
"title": "Datenmodell",
|
"title": "Datenmodell",
|
||||||
"order": 17,
|
"order": 17,
|
||||||
"fileEn": "datenmodell.en.md",
|
"fileEn": "datenmodell.en.md",
|
||||||
@@ -22,7 +22,7 @@ Der aktive Modus steht im Endpunkt
|
|||||||
Dein Profil (Avatar, Name, E-Mail, Tarif) siehst du unten links im
|
Dein Profil (Avatar, Name, E-Mail, Tarif) siehst du unten links im
|
||||||
Benutzermenü. Dort stehen dir folgende Aktionen zur Verfügung:
|
Benutzermenü. Dort stehen dir folgende Aktionen zur Verfügung:
|
||||||
|
|
||||||
- **Watchlist** — deine gespeicherten Tools (nur mit dem entsprechenden Tarif).
|
- **Merkliste** — deine gespeicherten Tools (nur mit dem entsprechenden Tarif).
|
||||||
- **Papierkorb** — wiederherstellbare, gelöschte Tools (Premium/Enterprise).
|
- **Papierkorb** — wiederherstellbare, gelöschte Tools (Premium/Enterprise).
|
||||||
- **Passwort ändern** — für lokale Konten direkt in toolr; für OIDC-Konten wird
|
- **Passwort ändern** — für lokale Konten direkt in toolr; für OIDC-Konten wird
|
||||||
die Passwortverwaltung im Identitätsanbieter angeboten.
|
die Passwortverwaltung im Identitätsanbieter angeboten.
|
||||||
@@ -47,7 +47,7 @@ API-Referenz: [`POST /auth/me/password`](/docs/reference/endpoints/auth#changeMy
|
|||||||
- die **Listenansicht** und **Dichte** im Bereich Tools durchsuchen anpassen
|
- die **Listenansicht** und **Dichte** im Bereich Tools durchsuchen anpassen
|
||||||
(siehe [Tools finden & durchsuchen](/docs/handbook/tools-finden)).
|
(siehe [Tools finden & durchsuchen](/docs/handbook/tools-finden)).
|
||||||
|
|
||||||
Deine Präferenzen (inkl. Watchlist) werden im Endpunkt
|
Deine Präferenzen (inkl. Merkliste) werden im Endpunkt
|
||||||
[`GET /auth/me/preferences`](/docs/reference/endpoints/auth#getMePreferences)
|
[`GET /auth/me/preferences`](/docs/reference/endpoints/auth#getMePreferences)
|
||||||
gespeichert und über [`PUT /auth/me/preferences`](/docs/reference/endpoints/auth#updateMePreferences)
|
gespeichert und über [`PUT /auth/me/preferences`](/docs/reference/endpoints/auth#updateMePreferences)
|
||||||
aktualisiert.
|
aktualisiert.
|
||||||
+3
-2
@@ -9,7 +9,7 @@ Feature-Beschränkungen.
|
|||||||
| Tarif | Beschreibung |
|
| Tarif | Beschreibung |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Free** | Grundfunktionen: suchen, filtern, ansehen, Analytics |
|
| **Free** | Grundfunktionen: suchen, filtern, ansehen, Analytics |
|
||||||
| **Premium** | Zusätzlich Watchlist, Vergleichen, Papierkorb, Kosten |
|
| **Premium** | Zusätzlich Merkliste, Vergleichen, Papierkorb, Kosten |
|
||||||
| **Enterprise** | Alle Premium-Features + erweiterter Support |
|
| **Enterprise** | Alle Premium-Features + erweiterter Support |
|
||||||
|
|
||||||
### Feature-Berechtigungen
|
### Feature-Berechtigungen
|
||||||
@@ -19,9 +19,10 @@ Premium/Enterprise schalten folgende Features frei:
|
|||||||
| Feature | Funktion | Mehr erfahren |
|
| Feature | Funktion | Mehr erfahren |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `compare` | Tools vergleichen | [Vergleichen](/docs/handbook/vergleichen) |
|
| `compare` | Tools vergleichen | [Vergleichen](/docs/handbook/vergleichen) |
|
||||||
| `watchlist` | Favoritenliste | [Watchlist](/docs/handbook/watchlist) |
|
| `watchlist` | Favoritenliste | [Merkliste](/docs/handbook/watchlist) |
|
||||||
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
| `trash` | Papierkorb (soft gelöschte Tools) | [Papierkorb](/docs/handbook/papierkorb) |
|
||||||
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
| `costs` | Kosten-/Lizenzmodelle erfassen | [Kosten erfassen](/docs/handbook/kosten) |
|
||||||
|
| `tool-import` | Tools per CSV/JSON/YAML importieren (Admin) | [Administration](/docs/handbook/administration) |
|
||||||
|
|
||||||
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
Fehlt dir ein Feature, zeigt die App einen **Upgrade-Hinweis** mit Link zur
|
||||||
Tarifverwaltung.
|
Tarifverwaltung.
|
||||||
+1
@@ -22,6 +22,7 @@ Premium/Enterprise unlock the following features:
|
|||||||
| `watchlist` | Favorites list | [Watchlist](/docs/handbook/watchlist) |
|
| `watchlist` | Favorites list | [Watchlist](/docs/handbook/watchlist) |
|
||||||
| `trash` | Trash (soft-deleted tools) | [Trash](/docs/handbook/papierkorb) |
|
| `trash` | Trash (soft-deleted tools) | [Trash](/docs/handbook/papierkorb) |
|
||||||
| `costs` | Record cost/license models | [Recording costs](/docs/handbook/kosten) |
|
| `costs` | Record cost/license models | [Recording costs](/docs/handbook/kosten) |
|
||||||
|
| `tool-import` | Import tools via CSV/JSON/YAML (admin) | [Administration](/docs/handbook/administration) |
|
||||||
|
|
||||||
If you are missing a feature, the app shows an **upgrade notice** with a link
|
If you are missing a feature, the app shows an **upgrade notice** with a link
|
||||||
to the plan management.
|
to the plan management.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user