Skip to content

Commit c0bb727

Browse files
authored
Merge pull request #5 from LAP-Tutorials/storage
Storage
2 parents ec6173a + 57ffb0c commit c0bb727

8 files changed

Lines changed: 1036 additions & 616 deletions

File tree

app/admin/articles/[id]/page.tsx

Lines changed: 434 additions & 280 deletions
Large diffs are not rendered by default.

app/admin/articles/new/page.tsx

Lines changed: 447 additions & 281 deletions
Large diffs are not rendered by default.

app/globals.css

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
@import "tailwindcss";
33

44
@theme {
5-
--font-sans: var(--font-general-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
5+
--font-sans: var(--font-general-sans), ui-sans-serif, system-ui, sans-serif,
6+
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
67
}
78

89
iframe[aria-hidden="true"] {
@@ -125,8 +126,6 @@ iframe[aria-hidden="true"] {
125126
}
126127
}
127128

128-
129-
130129
/* Text selection */
131130
::selection {
132131
background: #892be280;
@@ -146,7 +145,8 @@ iframe[aria-hidden="true"] {
146145

147146
/* Safe area insets for mobile devices */
148147
body {
149-
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
148+
padding: env(safe-area-inset-top) env(safe-area-inset-right)
149+
env(safe-area-inset-bottom) env(safe-area-inset-left);
150150
position: relative;
151151
z-index: 0;
152152
}
@@ -210,27 +210,27 @@ select:-webkit-autofill {
210210
/* Markdown Styles */
211211
.markdown-body {
212212
color: #fff !important;
213+
background-color: transparent !important;
214+
font-family: var(--font-general-sans), ui-sans-serif, system-ui, sans-serif !important;
213215
}
214216

215217
/* Heading */
216218
.markdown-body h1 {
217219
font-weight: 600;
218-
font-size: clamp(3rem, 2.7857rem + 1.0714vw, 4.5rem);
220+
font-size: clamp(2rem, 1.8rem + 1vw, 3rem);
221+
margin-bottom: 0.5em !important;
219222
margin-bottom: 0.5em !important;
220-
border-bottom: 1px solid #ffffff5b !important;
221-
padding-bottom: 0.1em !important;
222223
}
223224

224225
.markdown-body h2 {
225-
font-size: clamp(2.2rem, 2.0857rem + 0.6714vw, 3.3rem);
226+
font-size: clamp(1.5rem, 1.3rem + 0.8vw, 2.25rem);
226227
font-weight: 600;
227228
margin-bottom: 0.5em !important;
228-
border-bottom: 1px solid #ffffff5b !important;
229-
padding-bottom: 0.3em !important;
229+
margin-bottom: 0.5em !important;
230230
}
231231

232232
.markdown-body h3 {
233-
font-size: clamp(1.9rem, 1.8857rem + 0.4714vw, 2.8rem);
233+
font-size: clamp(1.2rem, 1.1rem + 0.5vw, 1.75rem);
234234
font-weight: 600;
235235
margin-bottom: 0.5em !important;
236236
}
@@ -444,4 +444,4 @@ iframe {
444444
.overflow-x-auto {
445445
-webkit-overflow-scrolling: touch;
446446
}
447-
}
447+
}

app/layout.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import type React from "react"
2-
import "./globals.css"
3-
import type { Metadata } from "next"
4-
import localFont from "next/font/local"
5-
import { Toaster } from "@/components/ui/toaster"
6-
import { Providers } from "./providers"
1+
import type React from "react";
2+
import "./globals.css";
3+
import type { Metadata } from "next";
4+
import localFont from "next/font/local";
5+
import { Fira_Code } from "next/font/google";
6+
import { Toaster } from "@/components/ui/toaster";
7+
import { Providers } from "./providers";
8+
9+
const firaCode = Fira_Code({
10+
subsets: ["latin"],
11+
variable: "--font-fira-code",
12+
display: "swap",
13+
});
714

815
const generalSans = localFont({
916
src: [
@@ -25,7 +32,7 @@ const generalSans = localFont({
2532
],
2633
variable: "--font-general-sans",
2734
display: "swap",
28-
})
35+
});
2936

3037
export const metadata: Metadata = {
3138
metadataBase: new URL("https://lap-cms.vercel.app"),
@@ -60,32 +67,34 @@ export const metadata: Metadata = {
6067
index: false,
6168
follow: false,
6269
},
63-
}
70+
};
6471

6572
export const viewport = {
6673
width: "device-width",
6774
initialScale: 1,
6875
maximumScale: 1,
6976
viewportFit: "cover",
7077
themeColor: "#121212",
71-
}
78+
};
7279

7380
export default function RootLayout({
7481
children,
7582
}: {
76-
children: React.ReactNode
83+
children: React.ReactNode;
7784
}) {
7885
return (
7986
<html lang="en" className="scroll-smooth">
8087
<head>
8188
<link rel="icon" href="/logos/LAP-Logo-Color.png" type="image/x-icon" />
8289
</head>
83-
<body className={`${generalSans.variable} font-sans`}>
90+
<body
91+
className={`${generalSans.variable} ${firaCode.variable} font-sans`}
92+
>
8493
<Providers>
8594
{children}
8695
<Toaster />
8796
</Providers>
8897
</body>
8998
</html>
90-
)
99+
);
91100
}

components/admin/assets/asset-manager.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useMemo, useEffect } from "react";
22
import { useAssets, Asset } from "@/hooks/use-assets";
33
import { UploadZone } from "./upload-zone";
4+
import { convertImageToWebP } from "@/lib/image-utils";
45
import { Button } from "@/components/ui/button";
56
import { Input } from "@/components/ui/input";
67
import {
@@ -199,13 +200,13 @@ const formatBytes = (bytes: number, decimals = 2) => {
199200
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
200201
};
201202

202-
export function AssetManager() {
203+
export function AssetManager({ rootPath = "" }: { rootPath?: string }) {
203204
// Root level management for Tabs
204205
const {
205206
folders: rootFolders,
206207
loading: rootLoading,
207208
refresh: refreshRoot,
208-
} = useAssets("");
209+
} = useAssets(rootPath);
209210
const [activeTab, setActiveTab] = useState("overview");
210211

211212
// Navigation state within a tab
@@ -216,6 +217,7 @@ export function AssetManager() {
216217
// If specific tab, path is tabName + subPath joined
217218
const getPath = () => {
218219
const parts = [];
220+
if (rootPath) parts.push(rootPath);
219221
if (activeTab !== "overview") parts.push(activeTab);
220222
if (subPath.length > 0) parts.push(...subPath);
221223
return parts.join("/");
@@ -464,9 +466,29 @@ export function AssetManager() {
464466
return folders;
465467
}, [folders, searchTerm]);
466468

467-
const handleUpload = (files: File[]) => {
468-
files.forEach((file) => uploadAsset(file));
469-
if (currentPath === "") {
469+
const handleUpload = async (files: File[]) => {
470+
// Convert images to WebP
471+
const processedFiles = await Promise.all(
472+
files.map(async (file) => {
473+
if (file.type.startsWith("image/")) {
474+
try {
475+
return await convertImageToWebP(file);
476+
} catch (e) {
477+
console.error("Failed to convert image to WebP", e);
478+
toast({
479+
title: "Image conversion failed",
480+
description: `Could not convert ${file.name} to WebP. Uploading original.`,
481+
variant: "destructive",
482+
});
483+
return file;
484+
}
485+
}
486+
return file;
487+
})
488+
);
489+
490+
processedFiles.forEach((file) => uploadAsset(file));
491+
if (activeTab === "overview" && subPath.length === 0) {
470492
setTimeout(refreshRoot, 2000);
471493
}
472494
};
@@ -499,7 +521,7 @@ export function AssetManager() {
499521
}
500522

501523
return (
502-
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
524+
<div className="grid grid-cols-[repeat(auto-fill,minmax(130px,1fr))] gap-4">
503525
{filteredAssets.map((asset) => (
504526
<div
505527
key={asset.id}
@@ -940,7 +962,7 @@ export function AssetManager() {
940962
<h3 className="text-sm font-medium text-white/50 mb-4 uppercase tracking-wider">
941963
Folders
942964
</h3>
943-
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
965+
<div className="grid grid-cols-[repeat(auto-fill,minmax(130px,1fr))] gap-4">
944966
{filteredFolders.map((folder) => (
945967
<div
946968
key={folder.id}

components/ui/button.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import * as React from "react"
2-
import { Slot } from "@radix-ui/react-slot"
3-
import { cva, type VariantProps } from "class-variance-authority"
1+
import * as React from "react";
2+
import { Slot } from "@radix-ui/react-slot";
3+
import { cva, type VariantProps } from "class-variance-authority";
44

5-
import { cn } from "@/lib/utils"
5+
import { cn } from "@/lib/utils";
66

77
const buttonVariants = cva(
88
"inline-flex items-center justify-center whitespace-nowrap rounded-none text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
99
{
1010
variants: {
1111
variant: {
1212
default: "bg-[#8a2be2] text-white hover:bg-[#7b26ca]",
13-
destructive: "bg-red-700 text-white hover:bg-red-800",
14-
outline: "border border-white bg-transparent hover:bg-white/10 text-white",
13+
destructive: "bg-[#dc2626] text-white hover:bg-[#b91c1c]",
14+
outline:
15+
"border border-white bg-transparent hover:bg-white/10 text-white",
1516
secondary: "bg-[#333333] text-white hover:bg-[#444444]",
1617
ghost: "text-white",
1718
link: "text-white underline-offset-4 hover:underline",
@@ -27,22 +28,27 @@ const buttonVariants = cva(
2728
variant: "default",
2829
size: "default",
2930
},
30-
},
31-
)
31+
}
32+
);
3233

3334
export interface ButtonProps
3435
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
3536
VariantProps<typeof buttonVariants> {
36-
asChild?: boolean
37+
asChild?: boolean;
3738
}
3839

3940
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
4041
({ className, variant, size, asChild = false, ...props }, ref) => {
41-
const Comp = asChild ? Slot : "button"
42-
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
43-
},
44-
)
45-
Button.displayName = "Button"
46-
47-
export { Button, buttonVariants }
42+
const Comp = asChild ? Slot : "button";
43+
return (
44+
<Comp
45+
className={cn(buttonVariants({ variant, size, className }))}
46+
ref={ref}
47+
{...props}
48+
/>
49+
);
50+
}
51+
);
52+
Button.displayName = "Button";
4853

54+
export { Button, buttonVariants };

components/ui/switch.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
"use client"
1+
"use client";
22

3-
import * as React from "react"
4-
import * as SwitchPrimitives from "@radix-ui/react-switch"
3+
import * as React from "react";
4+
import * as SwitchPrimitives from "@radix-ui/react-switch";
55

6-
import { cn } from "@/lib/utils"
6+
import { cn } from "@/lib/utils";
77

88
const Switch = React.forwardRef<
99
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -19,11 +19,11 @@ const Switch = React.forwardRef<
1919
>
2020
<SwitchPrimitives.Thumb
2121
className={cn(
22-
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
22+
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
2323
)}
2424
/>
2525
</SwitchPrimitives.Root>
26-
))
27-
Switch.displayName = SwitchPrimitives.Root.displayName
26+
));
27+
Switch.displayName = SwitchPrimitives.Root.displayName;
2828

29-
export { Switch }
29+
export { Switch };

lib/image-utils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Converts an image file to WebP format.
3+
* @param file The input image file.
4+
* @param quality The quality of the WebP image (0 to 1). Default is 0.8.
5+
* @returns A Promise that resolves to the converted File object.
6+
*/
7+
export const convertImageToWebP = (
8+
file: File,
9+
quality = 0.8
10+
): Promise<File> => {
11+
return new Promise((resolve, reject) => {
12+
// If it's already WebP, return as is
13+
if (file.type === "image/webp") {
14+
resolve(file);
15+
return;
16+
}
17+
18+
const img = new Image();
19+
const url = URL.createObjectURL(file);
20+
21+
img.onload = () => {
22+
URL.revokeObjectURL(url);
23+
const canvas = document.createElement("canvas");
24+
canvas.width = img.width;
25+
canvas.height = img.height;
26+
27+
const ctx = canvas.getContext("2d");
28+
if (!ctx) {
29+
reject(new Error("Could not get canvas context"));
30+
return;
31+
}
32+
33+
ctx.drawImage(img, 0, 0);
34+
35+
canvas.toBlob(
36+
(blob) => {
37+
if (blob) {
38+
const newFile = new File(
39+
[blob],
40+
file.name.replace(/\.[^/.]+$/, "") + ".webp",
41+
{
42+
type: "image/webp",
43+
lastModified: Date.now(),
44+
}
45+
);
46+
resolve(newFile);
47+
} else {
48+
reject(new Error("Conversion to WebP failed"));
49+
}
50+
},
51+
"image/webp",
52+
quality
53+
);
54+
};
55+
56+
img.onerror = () => {
57+
URL.revokeObjectURL(url);
58+
reject(new Error("Error loading image"));
59+
};
60+
61+
img.src = url;
62+
});
63+
};

0 commit comments

Comments
 (0)