Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
# Next.js template
# Image Optimizer

This is a Next.js template with shadcn/ui.
# Description
Image Optimizer is a web tool that offers different options to compress, resize, and optimize your images.

## Adding components
# Tech Stack

To add components to your app, run the following command:
<div align="center">

</div>

```bash
npx shadcn@latest add button
```

This will place the ui components in the `components` directory.

## Using components

To use the components in your app, import them as follows:

```tsx
import { Button } from "@/components/ui/button";
```
This tool is built using *Next.js* (App Router) and React.
28 changes: 10 additions & 18 deletions app/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { useEffect } from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { useImageStore } from "../../data/imageStore"
import ImageDiffViewer from "@/components/image-diff-viewer"
import ImageEditSettings from "@/components/image-edit-settings"

export default function EditPage() {
const router = useRouter()
Expand All @@ -13,24 +15,14 @@ export default function EditPage() {
if (!file) router.replace("/")
}, [file, router])

if (!file) return <div className="p-8">No image loaded. Redirecting…</div>

return (
<div className="p-8">
<h1 className="mb-4 text-xl font-semibold"></h1>
<div style={{ maxWidth: 1024 }}>
{url ? (
<Image
src={url}
alt="uploaded"
width={800}
height={600}
unoptimized
/>
) : (
<p>Preparing image…</p>
)}
</div>
return url === null ? null : (
<div className="grid h-full grid-cols-1 lg:grid-cols-12">
<ImageDiffViewer
beforeSrc={url}
afterSrc={"/public/hackerman.png"}
className="col-span-1 h-full lg:col-span-5"
/>
<ImageEditSettings />
</div>
)
}
45 changes: 45 additions & 0 deletions components/image-diff-viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"
import React from "react"
import {
ReactCompareSlider,
ReactCompareSliderImage,
ReactCompareSliderHandle,
} from "react-compare-slider"

type ImageDiffViewerProps = {
beforeSrc: string
afterSrc: string
alt?: string
className?: string
}

export default function ImageDiffViewer({
beforeSrc,
afterSrc,
alt = "",
className = "",
}: ImageDiffViewerProps) {
if (!beforeSrc || !afterSrc) return null

return (
<div className={`w-full max-w-full ${className}`}>
<ReactCompareSlider
itemOne={
<ReactCompareSliderImage
src={beforeSrc}
alt={alt ? `${alt} — before` : "before"}
style={{ objectFit: "contain", width: "100%", height: "100%" }}
/>
}
itemTwo={
<ReactCompareSliderImage
src={afterSrc}
alt={alt ? `${alt} — after` : "after"}
style={{ objectFit: "contain", width: "100%", height: "100%" }}
/>
}
handle={<ReactCompareSliderHandle />}
/>
</div>
)
}
7 changes: 7 additions & 0 deletions components/image-edit-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function ImageEditSettings() {
return (
<div className="flex flex-col gap-4">

</div>
)
}
4 changes: 2 additions & 2 deletions components/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function ImageUpload() {
const inputRef = useRef<HTMLInputElement | null>(null)
const timersRef = useRef<number[]>([])
const router = useRouter()
const setImageFile = useImageStore((s) => s.setFile)
const setOriginalFile = useImageStore((s) => s.setOriginalFile)

const errorWaitTime = 2000 // ms
const successWaitTime = 1500 // ms
Expand Down Expand Up @@ -82,7 +82,7 @@ export default function ImageUpload() {
}

try {
setImageFile(file)
setOriginalFile(file)
if (inputRef.current) inputRef.current.value = ""
setStatus("success")
pushTimeout(() => {
Expand Down
85 changes: 79 additions & 6 deletions data/imageStore.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,107 @@
import create from "zustand"

type ImageState = {
// Working copy (modifiable by editors/libraries)
file: File | null
url: string | null
setFile: (file: File) => void

// Original uploaded file (kept immutable)
originalFile: File | null
originalUrl: string | null

// Set the original file (called at upload time). This will also
// create a working copy so editors can modify `file` without
// mutating the original.
setOriginalFile: (file: File) => void

// Replace the working copy (e.g. when a library returns a modified Blob)
setWorkingFile: (fileOrBlob: File | Blob) => void

// Reset the working copy back to the original
resetWorking: () => void

clear: () => void

// Helpers to get ArrayBuffers from working/original
getArrayBuffer: () => Promise<ArrayBuffer | null>
getOriginalArrayBuffer: () => Promise<ArrayBuffer | null>
}

export const useImageStore = create<ImageState>((set, get) => ({
file: null,
url: null,
originalFile: null,
originalUrl: null,

setOriginalFile: (file: File) => {
// revoke previous original URL if present
const prevOriginal = get().originalUrl
if (prevOriginal) URL.revokeObjectURL(prevOriginal)
const originalUrl = URL.createObjectURL(file)

// revoke previous working URL if present
const prevWorking = get().url
if (prevWorking) URL.revokeObjectURL(prevWorking)

// create a new working copy (so modifications don't touch the original)
const workingCopy = new File([file], file.name, { type: file.type })
const workingUrl = URL.createObjectURL(workingCopy)

set({ originalFile: file, originalUrl, file: workingCopy, url: workingUrl })
},

setFile: (file: File) => {
// Revoke any previous object URL to avoid memory leaks
setWorkingFile: (fileOrBlob: File | Blob) => {
const prev = get().url
if (prev) URL.revokeObjectURL(prev)
const url = URL.createObjectURL(file)
set({ file, url })

let newFile: File
if (fileOrBlob instanceof File) {
newFile = fileOrBlob
} else {
const name = get().originalFile?.name ?? "edited"
const type =
(fileOrBlob as Blob).type ||
get().originalFile?.type ||
"application/octet-stream"
newFile = new File([fileOrBlob], name, { type })
}

const url = URL.createObjectURL(newFile)
set({ file: newFile, url })
},

resetWorking: () => {
const original = get().originalFile
const prev = get().url
if (prev) URL.revokeObjectURL(prev)
if (!original) {
set({ file: null, url: null })
return
}
const workingCopy = new File([original], original.name, {
type: original.type,
})
const workingUrl = URL.createObjectURL(workingCopy)
set({ file: workingCopy, url: workingUrl })
},

clear: () => {
const prevOriginal = get().originalUrl
if (prevOriginal) URL.revokeObjectURL(prevOriginal)
const prev = get().url
if (prev) URL.revokeObjectURL(prev)
set({ file: null, url: null })
set({ file: null, url: null, originalFile: null, originalUrl: null })
},

getArrayBuffer: async () => {
const f = get().file
if (!f) return null
return f.arrayBuffer()
},

getOriginalArrayBuffer: async () => {
const f = get().originalFile
if (!f) return null
return f.arrayBuffer()
},
}))
Loading
Loading