-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Login working in editor-implementation branch, but not in main?? Needs checking --------- Co-authored-by: 박세준 <[email protected]> Co-authored-by: Sejun Park <[email protected]> Co-authored-by: IceCandle <[email protected]>
- Loading branch information
1 parent
a548544
commit 4a190fe
Showing
44 changed files
with
1,038 additions
and
2,352 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
|
||
type Color = { | ||
r: number; | ||
g: number; | ||
b: number; | ||
}; | ||
|
||
const ImageProcessor = ({ | ||
file, | ||
onProcessed, | ||
}: { | ||
file: File | null; | ||
onProcessed: (blob: Blob) => void; | ||
}) => { | ||
const [processedImage, setProcessedImage] = useState<string | null>(null); | ||
const [hasProcessed, setHasProcessed] = useState(false); | ||
const canvasRef = useRef(null); | ||
|
||
const TARGET_WIDTH = 1080; | ||
const TARGET_HEIGHT = 1920; | ||
const TARGET_RATIO = TARGET_HEIGHT / TARGET_WIDTH; | ||
|
||
const extractColors = ( | ||
ctx: CanvasRenderingContext2D | null, | ||
width: number, | ||
height: number, | ||
): [Color, Color] => { | ||
if (ctx == null) | ||
return [ | ||
{ r: 0, g: 0, b: 0 }, | ||
{ r: 0, g: 0, b: 0 }, | ||
]; | ||
const imageData = ctx.getImageData(0, 0, width, height).data; | ||
const colors: Color[] = []; | ||
|
||
// Sample pixels at regular intervals | ||
const sampleSize = Math.floor(imageData.length / 1000); | ||
for (let i = 0; i < imageData.length; i += sampleSize * 4) { | ||
const r = imageData[i] ?? 0; | ||
const g = imageData[i + 1] ?? 0; | ||
const b = imageData[i + 2] ?? 0; | ||
colors.push({ r, g, b }); | ||
} | ||
|
||
// Get average color for muted background | ||
const avg: Color = colors.reduce( | ||
(acc, color) => ({ | ||
r: acc.r + color.r / colors.length, | ||
g: acc.g + color.g / colors.length, | ||
b: acc.b + color.b / colors.length, | ||
}), | ||
{ r: 0, g: 0, b: 0 }, | ||
); | ||
|
||
// Create slightly varied second color for gradient | ||
const secondColor: Color = { | ||
r: Math.min(255, avg.r * 0.8), | ||
g: Math.min(255, avg.g * 0.8), | ||
b: Math.min(255, avg.b * 0.8), | ||
}; | ||
|
||
return [avg, secondColor]; | ||
}; | ||
|
||
useEffect(() => { | ||
const processImage = async (imageFile: Blob | MediaSource) => { | ||
if (hasProcessed) return; | ||
const img = new Image(); | ||
img.src = URL.createObjectURL(imageFile); | ||
|
||
await new Promise((resolve) => { | ||
img.onload = resolve; | ||
}); | ||
|
||
const canvas = canvasRef.current as HTMLCanvasElement | null; | ||
if (canvas == null) return; | ||
const ctx = canvas.getContext('2d'); | ||
if (ctx == null) return; | ||
|
||
// Set canvas to target dimensions | ||
canvas.width = TARGET_WIDTH; | ||
canvas.height = TARGET_HEIGHT; | ||
|
||
// Extract colors from original image | ||
const tempCanvas = document.createElement('canvas'); | ||
const tempCtx = tempCanvas.getContext('2d'); | ||
if (tempCtx == null) return; | ||
tempCanvas.width = img.width; | ||
tempCanvas.height = img.height; | ||
tempCtx.drawImage(img, 0, 0); | ||
const [color1, color2] = extractColors(tempCtx, img.width, img.height); | ||
|
||
// Create gradient background | ||
const gradient = ctx.createLinearGradient(0, 0, 0, TARGET_HEIGHT); | ||
gradient.addColorStop( | ||
0, | ||
`rgba(${color1.r}, ${color1.g}, ${color1.b}, 0.8)`, | ||
); | ||
gradient.addColorStop( | ||
1, | ||
`rgba(${color2.r}, ${color2.g}, ${color2.b}, 0.8)`, | ||
); | ||
ctx.fillStyle = gradient; | ||
ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT); | ||
|
||
// Calculate dimensions to maintain aspect ratio | ||
let drawWidth = TARGET_WIDTH; | ||
let drawHeight = TARGET_HEIGHT; | ||
const imageRatio = img.height / img.width; | ||
|
||
if (imageRatio > TARGET_RATIO) { | ||
drawWidth = TARGET_HEIGHT / imageRatio; | ||
drawHeight = TARGET_HEIGHT; | ||
} else { | ||
drawWidth = TARGET_WIDTH; | ||
drawHeight = TARGET_WIDTH * imageRatio; | ||
} | ||
|
||
// Center the image | ||
const x = (TARGET_WIDTH - drawWidth) / 2; | ||
const y = (TARGET_HEIGHT - drawHeight) / 2; | ||
|
||
// Draw the image centered | ||
ctx.drawImage(img, x, y, drawWidth, drawHeight); | ||
|
||
// Convert to blob and create URL | ||
const blob = await new Promise<Blob | null>((resolve) => { | ||
canvas.toBlob(resolve, 'image/jpeg', 0.9); | ||
}); | ||
if (blob == null) return; | ||
const processedUrl = URL.createObjectURL(blob); | ||
setProcessedImage(processedUrl); | ||
setHasProcessed(true); | ||
onProcessed(blob); | ||
}; | ||
if (file != null && !hasProcessed) { | ||
void processImage(file); | ||
} | ||
}, [TARGET_RATIO, file, hasProcessed, onProcessed, processedImage]); | ||
|
||
return ( | ||
<div className="w-full max-w-lg mx-auto"> | ||
<canvas ref={canvasRef} className="hidden" /> | ||
{processedImage != null && ( | ||
<img | ||
src={processedImage} | ||
alt="Processed story" | ||
className="w-full h-auto rounded-lg shadow-lg" | ||
/> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default ImageProcessor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { useCallback, useState } from 'react'; | ||
import { useLocation, useNavigate } from 'react-router-dom'; | ||
|
||
import ImageProcessor from './ImageProcessor'; | ||
|
||
const StoryEditor = () => { | ||
const [isProcessing, setIsProcessing] = useState(false); | ||
const [error, setError] = useState<string | null>(null); | ||
const [isUploaded, setIsUploaded] = useState(false); | ||
const [processedBlob, setProcessedBlob] = useState<Blob | null>(null); | ||
const navigate = useNavigate(); | ||
const location = useLocation(); | ||
const state = location.state as { file: File }; | ||
|
||
const handleProcessed = useCallback((blob: Blob) => { | ||
setProcessedBlob(blob); | ||
}, []); | ||
|
||
const handleShare = async () => { | ||
if (isProcessing || isUploaded || processedBlob == null) return; | ||
|
||
setIsProcessing(true); | ||
setError(null); | ||
|
||
try { | ||
const formData = new FormData(); | ||
formData.append('files', processedBlob, 'story.jpg'); | ||
|
||
const response = await fetch( | ||
'https://waffle-instaclone.kro.kr/api/story/', | ||
{ | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${localStorage.getItem('access_token') ?? ''}`, | ||
}, | ||
body: formData, | ||
}, | ||
); | ||
|
||
if (!response.ok) { | ||
throw new Error('Failed to upload story'); | ||
} | ||
|
||
setIsUploaded(true); | ||
void navigate('/', { replace: true }); | ||
} catch (err) { | ||
setError( | ||
err instanceof Error ? err.message : 'An unknown error occurred', | ||
); | ||
} finally { | ||
setIsProcessing(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="min-h-screen bg-gray-100 p-4"> | ||
<div className="max-w-lg mx-auto"> | ||
<h1 className="text-2xl font-bold mb-4">Edit Story</h1> | ||
|
||
<ImageProcessor file={state.file} onProcessed={handleProcessed} /> | ||
|
||
{isProcessing && ( | ||
<div className="mt-4 text-center text-gray-600"> | ||
Processing and uploading your story... | ||
</div> | ||
)} | ||
|
||
{error != null && ( | ||
<div className="mt-4 text-center text-red-500">{error}</div> | ||
)} | ||
|
||
<div className="mt-4 flex justify-end space-x-2"> | ||
<button | ||
onClick={() => void navigate('/')} | ||
className="px-4 py-2 bg-gray-200 rounded-lg hover:bg-gray-300 transition-colors" | ||
> | ||
Cancel | ||
</button> | ||
<button | ||
onClick={() => void handleShare()} | ||
disabled={isProcessing || isUploaded || processedBlob == null} | ||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | ||
> | ||
Share Story | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default StoryEditor; |
Oops, something went wrong.