Skip to content

Commit

Permalink
Update example for image upload with clearance of storage and cdn fil…
Browse files Browse the repository at this point in the history
…es on every 5 minutes
  • Loading branch information
tirthbodawala committed Dec 31, 2024
1 parent 57db914 commit 2899b2b
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 127 deletions.
11 changes: 0 additions & 11 deletions apps/web/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,4 @@ export default [
// add more generic rule sets here, such as:
// js.configs.recommended,
...eslintPluginAstro.configs.recommended,
{
rules: {
// override/add rules settings here, such as:
// "astro/no-set-html-directive": "error"
},
languageOptions: {
parserOptions: {
project: "./tsconfig.json",
},
},
},
];
2 changes: 1 addition & 1 deletion apps/web/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Cloudflare App with Astro | Atyantik Technologies</title>
<link
href="https://fonts.googleapis.com/css?family=Raleway:100&display=swap"
href="https://fonts.googleapis.com/css?family=Raleway:100,400&display=swap"
rel="stylesheet"
type="text/css"
/>
Expand Down
42 changes: 0 additions & 42 deletions apps/web/src/pages/api/storage.ts

This file was deleted.

91 changes: 91 additions & 0 deletions apps/web/src/pages/gallery.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
import Layout from "@layouts/Layout.astro";
import { listStorageRecords } from "@services/database";
const cache = Astro.locals.runtime.env.CACHE;
let cachedStorageRecords = await cache.get("storage_records");
if (!cachedStorageRecords) {
const storageRecords = await listStorageRecords(Astro.locals.dbClient);
await cache.put("storage_records", JSON.stringify(storageRecords));
cachedStorageRecords = JSON.stringify(storageRecords);
}
const records = JSON.parse(cachedStorageRecords) as Awaited<
ReturnType<typeof listStorageRecords>
>;
---

<Layout>
<div class="container">
<div class="text-center">
<h1>The Gallery</h1>
<p class="alert">The gallery resets in every 5 minutes!</p>
<p><a href="/upload">Click here to upload new picture →</a></p>
</div>
<div class="masonry">
{
records.map((record) => (
<img src={`/cdn/${record.key}`} alt={record.originalName} />
))
}
</div>
</div>
</Layout>

<style>
a {
color: #6100ee;
font-weight: bold;
}
.container {
padding: 2rem;
}
.text-center {
text-align: center;
margin-bottom: 2rem;
}
.alert {
color: maroon;
font-weight: bold;
}
.masonry {
column-count: 1; /* Default: 1 column on very small screens */
column-gap: 1rem; /* Gutter size */
}

/* Make images fill the column’s width, with bottom margin as vertical spacing */
.masonry img {
width: 100%;
display: block;
margin-bottom: 1rem;
border-radius: 4px; /* optional styling */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* At >= 600px: 2 columns */
@media (min-width: 600px) {
.masonry {
column-count: 2;
}
}

/* At >= 900px: 3 columns */
@media (min-width: 900px) {
.masonry {
column-count: 3;
}
}

/* At >= 1200px: 4 columns */
@media (min-width: 1200px) {
.masonry {
column-count: 4;
}
}

/* At >= 1500px: 5 columns */
@media (min-width: 1500px) {
.masonry {
column-count: 5;
}
}
</style>
7 changes: 7 additions & 0 deletions apps/web/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const serverTime = Date.now();
⚡ Blazing Fast, Budget Friendly, Built to Scale
<p id="loadTime"></p>
</div>
<p>
<a href="/gallery">Checkout Gallery Example</a>
</p>
</div>
</Layout>
<script is:inline define:vars={{ serverTime }}>
Expand All @@ -20,6 +23,10 @@ const serverTime = Date.now();
`Page Load Time: ${loadTime}ms`;
</script>
<style>
a {
color: #6100ee;
font-weight: bold;
}
.center {
box-sizing: border-box;
display: flex;
Expand Down
168 changes: 168 additions & 0 deletions apps/web/src/pages/upload.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
---
import Layout from "@layouts/Layout.astro";
import { handleFile } from "../utils/upload.util";
const MAX_FILE_SIZE = 1024 * 1024 * 5; // 5MB
if (Astro.request.method === "POST") {
try {
const data = await Astro.request.formData();
const image = data.get("image");
if (image instanceof File) {
if (!image.type.startsWith("image/")) {
throw new Error("Invalid file type");
}
if (image.size > MAX_FILE_SIZE) {
throw new Error("File size too large");
}
const fileData = await handleFile(image, Astro.locals);
console.log(fileData);
return Astro.redirect("/gallery");
}
} catch {
return Astro.redirect("/error");
}
}
---

<Layout>
<div class="center">
<form method="post" enctype="multipart/form-data" action="">
<label class="upload-box" for="file">
<div class="show-on-submit loader"></div>
<div class="hide-on-submit">
<span>Click to Upload</span>
Upload a picture to the gallery
<input type="file" id="file" name="image" accept="image/*" />
</div>
</label>
</form>
<div class="hide-on-submit view-gallery">
<a href="/gallery">Or click to view the Gallery</a>
</div>
</div>
</Layout>
<script is:inline define:vars={{ MAX_FILE_SIZE }}>
document
.querySelector("input[type=file]")
?.addEventListener?.("change", (e) => {
if (e?.target instanceof HTMLInputElement) {
const form = e.target.closest("form");
if (e.target instanceof File) {
if (!e.target.type.startsWith("image/")) {
alert("Only images are allowed");
form?.reset();
return;
}

if (e.target.size > MAX_FILE_SIZE) {
alert("File size too large. Max 5MB allowed");
form?.reset();
return;
}
}
if (form) {
form.disabled = true;
form.classList.add("loading");
form.submit();
}
}
});
</script>
<style>
h1 {
margin: 0 0 1rem 0;
padding: 0;
font-size: 1.5rem;
}
.center {
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* Full viewport height */
flex-direction: column;
padding: 10px;
text-align: center;
background-color: #f7f7f7; /* optional background color */
}

/* The clickable box (label) for file upload */
.upload-box {
display: block;
border: 2px dashed #ccc;
border-radius: 10px;
width: 300px; /* fixed width so it doesn’t shrink too much */
padding: 2rem;
font-size: 1.2rem;
color: #555;
background-color: #fff;
cursor: pointer;
transition:
border-color 0.2s ease-in-out,
background 0.2s ease-in-out;
}

/* Hover/focus states */
.upload-box:hover {
border-color: #6100ee;
background-color: #f0f8ff; /* light highlight */
}
.upload-box span {
display: block;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1rem;
}

/* Visually hide the real file input */
.upload-box input[type="file"] {
display: none;
}
.view-gallery {
margin-top: 1rem;
}
.view-gallery a {
color: #6100ee;
text-decoration: none;
}

.loader {
height: 4px;
width: 100%;
--c: no-repeat linear-gradient(#6100ee 0 0);
background: var(--c), var(--c), #d7b8fc;
background-size: 60% 100%;
animation: l16 3s infinite;
}

.loading .hide-on-submit {
display: none;
}

.show-on-submit {
display: none;
}
.loading .show-on-submit {
display: block;
}

@keyframes l16 {
0% {
background-position:
-150% 0,
-150% 0;
}
66% {
background-position:
250% 0,
-150% 0;
}
100% {
background-position:
250% 0,
250% 0;
}
}
</style>
24 changes: 1 addition & 23 deletions apps/web/src/utils/r2-storage.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { R2Bucket } from "@cloudflare/workers-types";
*/
export function validateFile(file: File | null): File {
if (!file || !(file instanceof File)) {
throw new Error("No image file provided or invalid file type.");
throw new Error("No file provided or invalid file type.");
}
return file;
}
Expand Down Expand Up @@ -50,25 +50,3 @@ export async function uploadFile(
},
});
}

/**
* Constructs the CDN URL based on the environment.
* @param mode - The current environment mode ('production' or others).
* @param cdnUrl - The CDN base URL from environment variables.
* @param requestUrl - The original request URL.
* @param key - The unique key of the uploaded file.
* @returns The full CDN URL as a string.
*/
export function constructCdnUrl(
mode: string,
cdnUrl: string | undefined,
requestUrl: string,
key: string,
): string {
const isProduction = mode === "production";
const baseCdnUrl =
isProduction && typeof cdnUrl === "string" && cdnUrl.length
? cdnUrl
: new URL("/cdn/", requestUrl);
return new URL(key, baseCdnUrl).toString();
}
Loading

0 comments on commit 2899b2b

Please sign in to comment.