freezmo/components/icons.tsx
Martin Dvorak 3ea2cea100 Freezmo: self-hosted notes-to-activities study app
Next.js 15 app that turns photos, PDFs, DOCX/PPTX and text into a swipeable
feed of AI-generated activities (flashcards, multiple choice, true/false,
fill-in-the-blank, matching, ordering, short answer) using the user's own
Ollama models.

Highlights:
- All AI calls run in server route handlers; the browser never talks to Ollama.
- Bring-your-own-key mode (default): the key lives only in the browser and is
  sent per-request; server, local-daemon and shared-key modes also supported.
- Client-side document extraction (pdf.js, mammoth, jszip) with scanned-PDF
  page rendering for the vision model; notes stay in their original language.
- ChatGPT-style shell: collapsible sidebar / mobile drawer, neutral token-based
  light + dark themes, folder organisation, PWA (manifest, SW, offline page).
- Strict JSON validation with zod + auto-repair, rate limiting, and request
  guards (cross-origin, body-size, API-key sanitising).
- setup.sh bootstraps deps, .env.local and a production build.

Gitignore excludes .env*, the generated pdf.js worker and local scratch files.
2026-10-06 20:56:55 +00:00

239 lines
6.5 KiB
TypeScript

"use client";
/**
* Small inline SVG icons used for the app chrome so that critical controls
* don't depend on a system emoji font being installed.
*/
type IconProps = { className?: string; strokeWidth?: number };
const base = "size-5";
function Svg({
children,
className,
filled,
}: {
children: React.ReactNode;
className?: string;
filled?: boolean;
}) {
return (
<svg
viewBox="0 0 24 24"
className={className ?? base}
fill={filled ? "currentColor" : "none"}
stroke={filled ? "none" : "currentColor"}
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{children}
</svg>
);
}
export function IceCubeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.2 19 7v10l-7 3.8L5 17V7l7-3.8Z" />
<path d="M5 7l7 3.8L19 7M12 10.8V21" />
</Svg>
);
}
export function SnowflakeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 2v20M4.5 6.5l15 11M19.5 6.5l-15 11" />
<path d="M12 6l-2.2-2.2M12 6l2.2-2.2M12 18l-2.2 2.2M12 18l2.2 2.2M7 9.2 4.8 8.6M7 14.8l-2.2.6M17 9.2l2.2-.6M17 14.8l2.2.6" />
</Svg>
);
}
export function GearIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="3.2" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.11A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.7 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.11A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.7a1.7 1.7 0 0 0 1.03-1.56V3a2 2 0 1 1 4 0v.11A1.7 1.7 0 0 0 15.1 4.7a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.03A1.7 1.7 0 0 0 21 10.06H21a2 2 0 1 1 0 4h-.11A1.7 1.7 0 0 0 19.4 15Z" />
</Svg>
);
}
export function SunIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</Svg>
);
}
export function MoonIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z" />
</Svg>
);
}
export function BoltIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M13.5 2 4 13.2h5.4L9.5 22 19 10.8h-5.4L13.5 2Z" />
</Svg>
);
}
export function FlameIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M12 2s4.5 4.2 4.5 8.2c0 1.2-.5 2.2-1.2 2.9.1-1.6-.5-3.3-1.8-4.6.3 2.6-1 4-2.2 5.1-1 .9-1.8 1.8-1.8 3.2 0 1 .4 1.9 1.1 2.6-2.9-.3-5.1-2.6-5.1-5.5C5.5 9.7 12 8 12 2Z" />
<path d="M12 22c2.2 0 4-1.6 4-3.7 0-1.6-.8-2.4-1.7-3.3-.6-.6-1.3-1.3-1.6-2.3-.8.8-1.4 2-1.4 3.2 0 .9-.4 1.4-1 1.8-.5.4-1.1.9-1.1 1.9C9.2 21.1 10.4 22 12 22Z" />
</Svg>
);
}
export function SparklesIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z" />
<path d="M18.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8.8-2.2Z" />
</Svg>
);
}
export function LayersIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.5 20 8l-8 4.5L4 8l8-4.5Z" />
<path d="m4.6 12.4 7.4 4.2 7.4-4.2M4.6 16.4l7.4 4.2 7.4-4.2" />
</Svg>
);
}
export function FolderIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.5h9A1.5 1.5 0 0 1 21 10v8a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18V7.5Z" />
</Svg>
);
}
export function CameraIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h1.9l1.1-1.8A1 1 0 0 1 9.4 5h5.2a1 1 0 0 1 .9.5L16.6 7h1.9A1.5 1.5 0 0 1 20 8.5v9A1.5 1.5 0 0 1 18.5 19h-13A1.5 1.5 0 0 1 4 17.5v-9Z" />
<circle cx="12" cy="12.8" r="3.2" />
</Svg>
);
}
export function PlayIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M8 5.5v13l11-6.5-11-6.5Z" />
</Svg>
);
}
export function DownloadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M7.5 10.5 12 15l4.5-4.5M4 20h16" />
</Svg>
);
}
export function UploadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 16V4M7.5 8.5 12 4l4.5 4.5M4 20h16" />
</Svg>
);
}
export function HomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4v-5H9v5H5a1 1 0 0 1-1-1v-8.5Z" />
</Svg>
);
}
export function CheckCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m8.2 12.4 2.6 2.6 5-5.4" />
</Svg>
);
}
export function AlertTriangleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 4.5 21 19.5H3L12 4.5Z" />
<path d="M12 10v4.2M12 17.2v.1" />
</Svg>
);
}
export function XCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</Svg>
);
}
export function PlusIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 5v14M5 12h14" />
</Svg>
);
}
export function ShareIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M8.2 6.8 12 3l3.8 3.8" />
<path d="M6 12H5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-1" />
</Svg>
);
}
export function ChevronLeftIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M14.5 5.5 8 12l6.5 6.5" />
</Svg>
);
}
export function MenuIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 7h16M4 12h16M4 17h16" />
</Svg>
);
}
export function ChevronRightIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m9.5 5.5 6.5 6.5-6.5 6.5" />
</Svg>
);
}
export function RefreshIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M20 12a8 8 0 1 1-2.4-5.7M20 4v4h-4" />
</Svg>
);
}