uploads saved

main
miranshala 7 months ago
parent bfedd5fdaa
commit d1bc2139b4

@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";
import { getPool } from "@/lib/database";
import { promises as fs } from "fs";
export async function GET(request: NextRequest) {
const userId = request.headers.get("user-id");
if (!userId) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
try {
const pool = getPool();
const result = await pool.query(
`SELECT id, original_name, file_path, upload_status, created_at
FROM uploaded_files
WHERE user_id = $1
ORDER BY created_at DESC`,
[userId]
);
return NextResponse.json({ files: result.rows });
} catch (err: any) {
return NextResponse.json(
{ error: err.message || "Server error" },
{ status: 500 }
);
}
}
export async function DELETE(request: NextRequest) {
const userId = request.headers.get("user-id");
if (!userId) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
try {
const body = await request.json();
const fileId: string | undefined = body?.id;
if (!fileId) {
return NextResponse.json({ error: "Missing file id" }, { status: 400 });
}
const pool = getPool();
// Fetch file to validate ownership and get path
const fileRes = await pool.query(
`SELECT id, user_id, file_path FROM uploaded_files WHERE id = $1`,
[fileId]
);
if (fileRes.rows.length === 0) {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
const fileRow = fileRes.rows[0];
if (fileRow.user_id !== userId) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Try to delete file from disk (best-effort)
if (fileRow.file_path) {
try {
await fs.unlink(fileRow.file_path);
} catch (_) {
// ignore missing files
}
}
// Delete DB row
await pool.query(`DELETE FROM uploaded_files WHERE id = $1`, [fileId]);
return NextResponse.json({ success: true });
} catch (err: any) {
return NextResponse.json(
{ error: err.message || "Server error" },
{ status: 500 }
);
}
}

@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from "next/server";
import { promises as fs } from "fs";
import path from "path";
import { getPool } from "@/lib/database";
import { randomUUID } from "crypto";
export const config = {
api: {
bodyParser: false, // We handle parsing
},
};
export async function POST(req: NextRequest) {
// Get user-id (for dev); in real world, extract from session/token
const userId = req.headers.get("user-id");
if (!userId) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
// Parse the form data
try {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
// Only allow .blend, .zip, .jpg, .png, .svg for now
const filename = file.name;
if (!filename.match(/\.(blend|zip|jpg|jpeg|png|svg)$/i)) {
return NextResponse.json(
{ error: "File type not allowed" },
{ status: 400 }
);
}
const saveAs = randomUUID() + path.extname(filename);
const uploadsDir = path.resolve(process.cwd(), "uploads");
await fs.mkdir(uploadsDir, { recursive: true });
const filepath = path.join(uploadsDir, saveAs);
const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer);
// Write metadata to uploaded_files
const pool = getPool();
const result = await pool.query(
`INSERT INTO uploaded_files (user_id, original_name, file_path, upload_status) VALUES ($1, $2, $3, $4) RETURNING *`,
[userId, filename, filepath, "COMPLETE"]
);
const uploadedFile = result.rows[0];
return NextResponse.json({ success: true, file: uploadedFile });
} catch (err: any) {
console.error(err);
return NextResponse.json(
{ error: err.message || "Upload failed" },
{ status: 500 }
);
}
}

@ -0,0 +1,168 @@
"use client";
import { useEffect, useState } from "react";
import { useAuth } from "@/lib/auth-context";
import { useRouter } from "next/navigation";
interface UploadedFile {
id: string;
original_name: string;
file_path: string;
upload_status: string;
created_at: string;
}
export default function RendersPage() {
const { isAuthenticated, user } = useAuth();
const router = useRouter();
const [files, setFiles] = useState<UploadedFile[]>([]);
const [loading, setLoading] = useState(true);
const [authInitialized, setAuthInitialized] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
useEffect(() => {
const timer = setTimeout(() => setAuthInitialized(true), 100);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!authInitialized) return;
if (!isAuthenticated || !user) {
router.push("/login?redirect=/renders");
return;
}
fetchFiles();
// eslint-disable-next-line
}, [authInitialized, isAuthenticated, user]);
async function fetchFiles() {
try {
const res = await fetch("/api/renders/in", {
headers: { "user-id": user?.id },
});
const data = await res.json();
setFiles(data.files || []);
} catch {
setFiles([]);
} finally {
setLoading(false);
}
}
async function handleDelete(id: string) {
if (!user) return;
const ok = confirm("Delete this file? This cannot be undone.");
if (!ok) return;
setDeletingId(id);
try {
const res = await fetch("/api/renders/in", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"user-id": user.id,
},
body: JSON.stringify({ id }),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Delete failed");
await fetchFiles();
} catch (e) {
alert((e as any).message || "Delete failed");
} finally {
setDeletingId(null);
}
}
if (!authInitialized || !isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center">
Loading...
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-5xl mx-auto px-4">
<h1 className="text-4xl font-bold text-gray-900 mb-8">My Renders</h1>
<section className="mb-12">
<h2 className="text-2xl font-bold text-orange-600 mb-4">
Uploaded Files
</h2>
{loading ? (
<div>Loading...</div>
) : files.length === 0 ? (
<div className="text-gray-500">No files uploaded yet.</div>
) : (
<table className="w-full bg-white rounded-lg shadow-md">
<thead>
<tr className="bg-orange-50">
<th className="p-3 text-left font-semibold">Filename</th>
<th className="p-3 text-left font-semibold">Status</th>
<th className="p-3 text-left font-semibold">Uploaded At</th>
<th className="p-3 text-left font-semibold">Download</th>
<th className="p-3 text-left font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{files.map((f) => (
<tr key={f.id} className="border-t">
<td className="p-3">{f.original_name}</td>
<td className="p-3 capitalize">
<span
className={`px-2 py-1 rounded-full text-xs font-bold ${
f.upload_status === "COMPLETE"
? "bg-green-100 text-green-800"
: "bg-gray-200 text-gray-600"
}`}
>
{f.upload_status}
</span>
</td>
<td className="p-3">
{new Date(f.created_at).toLocaleString()}
</td>
<td className="p-3">
<a
href={
f.file_path
? `/uploads/${f.file_path.split("/").pop()}`
: "#"
}
className="underline text-blue-700"
target="_blank"
rel="noopener noreferrer"
download={f.original_name}
>
Download
</a>
</td>
<td className="p-3">
<button
className="px-3 py-1 text-sm rounded-md bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300"
disabled={deletingId === f.id}
onClick={() => handleDelete(f.id)}
>
{deletingId === f.id ? "Deleting..." : "Delete"}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</section>
<section>
<h2 className="text-2xl font-bold text-gray-600 mb-4">
Rendered Files (outputs)
</h2>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center text-gray-400">
Placeholder: Your rendered products will appear here in the future!
</div>
</section>
</div>
</div>
);
}

@ -228,28 +228,6 @@ export default function SubscriptionsPage() {
</div>
</div>
{/* Storage Usage Bar */}
<div className="mb-6">
<p className="text-sm text-gray-500 mb-2">Storage Usage</p>
<div className="w-full bg-gray-200 rounded-full h-4">
<div
className="bg-orange-500 h-4 rounded-full"
style={{
width: `${Math.min(
(parseFloat(subscription.current_usage_gb) /
subscription.storage_limit_gb) *
100,
100
)}%`,
}}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
{parseFloat(subscription.current_usage_gb).toFixed(2)} GB of{" "}
{subscription.storage_limit_gb} GB used
</p>
</div>
{/* Billing Info */}
<div className="border-t pt-4">
<p className="text-sm text-gray-500 mb-1">Member Since</p>

@ -1,9 +1,8 @@
"use client";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/lib/auth-context";
import { useState } from "react";
import UploadArea from "@/components/UploadArea";
export default function UploadPage() {
@ -11,6 +10,28 @@ export default function UploadPage() {
const router = useRouter();
const [userSubscription, setUserSubscription] = useState<any>(null);
const [authInitialized, setAuthInitialized] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [uploading, setUploading] = useState(false);
const [uploadMessage, setUploadMessage] = useState<string | null>(null);
function handleFilesSelected(newFiles: File[]) {
setSelectedFiles((prev) => {
const merged = [...prev];
for (const file of newFiles) {
if (
!merged.some(
(f) =>
f.name === file.name &&
f.size === file.size &&
f.lastModified === file.lastModified
)
) {
merged.push(file);
}
}
return merged;
});
}
// Wait for auth to initialize from localStorage
useEffect(() => {
@ -66,6 +87,35 @@ export default function UploadPage() {
const hasActiveSubscription =
userSubscription && userSubscription.status === "ACTIVE";
async function handleStartUpload() {
if (!selectedFiles.length || !hasActiveSubscription || !user) return;
setUploading(true);
setUploadMessage(null);
try {
for (const file of selectedFiles) {
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", {
method: "POST",
headers: {
"user-id": user.id,
},
body: formData,
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Upload failed");
}
setUploadMessage("Upload successful!");
setSelectedFiles([]); // Reset picker
// Optionally: also refresh renders dashboard, etc
} catch (err: any) {
setUploadMessage(err.message || "Upload failed");
} finally {
setUploading(false);
}
}
return (
<div className="min-h-screen bg-gray-50 py-12">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
@ -98,33 +148,6 @@ export default function UploadPage() {
</div>
)}
{/* Subscription Info - Only show if active */}
{hasActiveSubscription && userSubscription && (
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-2">
Your {userSubscription.tier} Plan
</h2>
<p className="text-sm text-gray-600">
Storage:{" "}
{parseFloat(userSubscription.current_usage_gb).toFixed(2)} GB /{" "}
{userSubscription.storage_limit_gb} GB used
</p>
<div className="mt-3 w-full bg-gray-200 rounded-full h-2">
<div
className="bg-orange-500 h-2 rounded-full"
style={{
width: `${Math.min(
(parseFloat(userSubscription.current_usage_gb) /
userSubscription.storage_limit_gb) *
100,
100
)}%`,
}}
></div>
</div>
</div>
)}
{/* Upload Area */}
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
@ -135,8 +158,45 @@ export default function UploadPage() {
!hasActiveSubscription ? "pointer-events-none opacity-50" : ""
}
>
<UploadArea className="w-full shadow-sm" />
<UploadArea
className="w-full shadow-sm"
onFilesSelected={handleFilesSelected}
/>
</div>
{/* Selected files indicator */}
{selectedFiles.length > 0 && (
<div className="mt-6 border rounded-md p-4 bg-gray-50">
<div className="font-semibold text-gray-800 mb-2">
Selected files ({selectedFiles.length}):
</div>
<ul className="list-disc list-inside text-sm text-gray-700 space-y-1 max-h-40 overflow-auto">
{selectedFiles.map((f, idx) => (
<li key={idx}>
{f.name}{" "}
<span className="text-gray-500">
({(f.size / 1024).toFixed(1)} KB)
</span>
</li>
))}
</ul>
</div>
)}
<button
className="mt-8 px-8 py-3 rounded-md font-bold bg-orange-500 text-white hover:bg-orange-600 disabled:bg-gray-300 disabled:text-gray-500"
disabled={
!hasActiveSubscription || uploading || !selectedFiles.length
}
onClick={handleStartUpload}
>
{uploading ? "Uploading..." : "Start"}
</button>
{uploadMessage && (
<div className="mt-4 text-center text-green-600">
{uploadMessage}
</div>
)}
</div>
</div>
</div>

@ -3,7 +3,7 @@
import React, { useRef, useState } from "react";
type UploadAreaProps = {
onFilesSelected?: (files: FileList) => void;
onFilesSelected?: (files: File[]) => void;
className?: string;
};
@ -29,13 +29,13 @@ export default function UploadArea({
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer?.files?.length) {
onFilesSelected?.(e.dataTransfer.files);
onFilesSelected?.(Array.from(e.dataTransfer.files));
}
};
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.files?.length) {
onFilesSelected?.(e.target.files);
onFilesSelected?.(Array.from(e.target.files));
// reset input to allow re-selecting the same file
e.currentTarget.value = "";
}
@ -81,6 +81,7 @@ export default function UploadArea({
ref={fileInputRef}
type="file"
multiple
accept=".blend,.zip,.jpg,.jpeg,.png,.svg"
className="hidden"
onChange={handleChange}
/>

@ -0,0 +1,24 @@
-- IN: Table for uploaded files
CREATE TABLE IF NOT EXISTS uploaded_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
original_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
upload_status VARCHAR(50) DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
render_job_id UUID -- nullable, links to rendered_files
);
-- OUT: Table for rendered files corresponding to uploads
CREATE TABLE IF NOT EXISTS rendered_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
uploaded_file_id UUID REFERENCES uploaded_files(id) ON DELETE CASCADE NOT NULL,
render_status VARCHAR(50) DEFAULT 'PENDING',
output_file_path VARCHAR(500),
rendered_at TIMESTAMP,
error_message TEXT
);
-- Some useful indices
CREATE INDEX IF NOT EXISTS idx_uploaded_files_user_id ON uploaded_files(user_id);
CREATE INDEX IF NOT EXISTS idx_rendered_files_uploaded_file_id ON rendered_files(uploaded_file_id);

@ -0,0 +1,43 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1366.000000pt" height="768.000000pt" viewBox="0 0 1366.000000 768.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,768.000000) scale(0.100000,-0.100000)"
fill="#FF8000" stroke="none">
<path d="M3120 4630 l-335 -580 -248 -2 -248 -3 -221 -380 c-121 -209 -222
-387 -225 -395 -2 -9 97 -191 221 -405 l225 -390 1670 -3 1671 -2 0 205 0 205
-827 0 c-456 0 -891 3 -969 7 l-141 6 -266 461 c-146 254 -269 469 -272 478
-8 19 -20 -3 313 576 l226 392 556 0 556 0 84 -147 c47 -81 86 -152 88 -158 1
-6 -101 -191 -228 -410 l-230 -399 0 -103 0 -103 178 0 177 0 169 295 169 295
346 0 347 0 169 -295 170 -295 238 0 c153 0 237 4 235 10 -1 5 -131 232 -288
504 l-285 495 -345 1 -345 0 -205 358 -205 357 -795 3 -795 2 -335 -580z
m-332 -1002 c12 -16 412 -707 419 -725 4 -11 -61 -13 -338 -11 l-343 3 -102
175 c-56 96 -103 181 -103 189 -1 8 47 96 105 197 l106 184 123 0 c86 0 126
-4 133 -12z"/>
<path d="M9414 4643 c4 -9 124 -234 266 -501 l260 -484 0 -314 0 -314 195 0
195 0 0 323 0 322 208 385 c115 212 234 433 266 493 l58 107 -205 0 -204 0
-157 -321 c-87 -176 -161 -318 -164 -314 -4 4 -75 147 -158 318 l-152 312
-207 3 c-191 2 -207 1 -201 -15z"/>
<path d="M7868 4281 c-26 -10 -63 -28 -82 -40 -36 -23 -146 -137 -146 -153 0
-4 -4 -8 -10 -8 -6 0 -10 40 -10 100 l0 100 -180 0 -180 0 0 -625 0 -625 190
0 190 0 0 324 c0 178 5 348 10 377 17 88 55 138 134 179 67 34 73 35 183 35
94 0 113 3 113 15 0 8 7 84 15 169 8 84 12 157 9 162 -10 17 -186 9 -236 -10z"/>
<path d="M8643 4285 c-205 -45 -356 -196 -410 -413 -22 -86 -24 -342 -4 -422
41 -169 146 -301 293 -370 124 -58 220 -74 403 -67 156 6 238 21 333 63 l52
23 0 137 c0 126 -1 136 -17 129 -262 -103 -481 -113 -600 -26 -46 33 -88 110
-99 179 l-7 42 403 0 403 0 -6 148 c-10 222 -45 329 -142 431 -104 109 -246
162 -434 160 -58 0 -134 -7 -168 -14z m270 -259 c66 -27 127 -127 127 -205 0
-21 -5 -21 -220 -21 -250 0 -233 -7 -205 88 30 99 101 151 208 152 32 0 72 -6
90 -14z"/>
<path d="M11164 4285 c-85 -13 -197 -46 -250 -73 l-32 -17 56 -123 c43 -95 59
-122 71 -118 9 3 55 19 102 36 152 53 286 59 353 15 34 -22 56 -75 59 -137 l2
-53 -170 -7 c-234 -9 -359 -38 -460 -108 -94 -64 -135 -157 -135 -301 0 -249
141 -388 392 -389 153 0 247 36 343 131 33 32 64 59 68 59 4 0 7 -38 7 -85 l0
-85 166 0 165 0 -3 473 c-4 464 -4 473 -27 535 -45 123 -162 216 -310 248 -81
17 -286 17 -397 -1z m366 -747 c0 -124 -37 -194 -124 -234 -89 -41 -190 -30
-241 26 -28 31 -28 137 1 176 50 67 122 92 272 93 l92 1 0 -62z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

@ -0,0 +1,43 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1366.000000pt" height="768.000000pt" viewBox="0 0 1366.000000 768.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,768.000000) scale(0.100000,-0.100000)"
fill="#FF8000" stroke="none">
<path d="M3120 4630 l-335 -580 -248 -2 -248 -3 -221 -380 c-121 -209 -222
-387 -225 -395 -2 -9 97 -191 221 -405 l225 -390 1670 -3 1671 -2 0 205 0 205
-827 0 c-456 0 -891 3 -969 7 l-141 6 -266 461 c-146 254 -269 469 -272 478
-8 19 -20 -3 313 576 l226 392 556 0 556 0 84 -147 c47 -81 86 -152 88 -158 1
-6 -101 -191 -228 -410 l-230 -399 0 -103 0 -103 178 0 177 0 169 295 169 295
346 0 347 0 169 -295 170 -295 238 0 c153 0 237 4 235 10 -1 5 -131 232 -288
504 l-285 495 -345 1 -345 0 -205 358 -205 357 -795 3 -795 2 -335 -580z
m-332 -1002 c12 -16 412 -707 419 -725 4 -11 -61 -13 -338 -11 l-343 3 -102
175 c-56 96 -103 181 -103 189 -1 8 47 96 105 197 l106 184 123 0 c86 0 126
-4 133 -12z"/>
<path d="M9414 4643 c4 -9 124 -234 266 -501 l260 -484 0 -314 0 -314 195 0
195 0 0 323 0 322 208 385 c115 212 234 433 266 493 l58 107 -205 0 -204 0
-157 -321 c-87 -176 -161 -318 -164 -314 -4 4 -75 147 -158 318 l-152 312
-207 3 c-191 2 -207 1 -201 -15z"/>
<path d="M7868 4281 c-26 -10 -63 -28 -82 -40 -36 -23 -146 -137 -146 -153 0
-4 -4 -8 -10 -8 -6 0 -10 40 -10 100 l0 100 -180 0 -180 0 0 -625 0 -625 190
0 190 0 0 324 c0 178 5 348 10 377 17 88 55 138 134 179 67 34 73 35 183 35
94 0 113 3 113 15 0 8 7 84 15 169 8 84 12 157 9 162 -10 17 -186 9 -236 -10z"/>
<path d="M8643 4285 c-205 -45 -356 -196 -410 -413 -22 -86 -24 -342 -4 -422
41 -169 146 -301 293 -370 124 -58 220 -74 403 -67 156 6 238 21 333 63 l52
23 0 137 c0 126 -1 136 -17 129 -262 -103 -481 -113 -600 -26 -46 33 -88 110
-99 179 l-7 42 403 0 403 0 -6 148 c-10 222 -45 329 -142 431 -104 109 -246
162 -434 160 -58 0 -134 -7 -168 -14z m270 -259 c66 -27 127 -127 127 -205 0
-21 -5 -21 -220 -21 -250 0 -233 -7 -205 88 30 99 101 151 208 152 32 0 72 -6
90 -14z"/>
<path d="M11164 4285 c-85 -13 -197 -46 -250 -73 l-32 -17 56 -123 c43 -95 59
-122 71 -118 9 3 55 19 102 36 152 53 286 59 353 15 34 -22 56 -75 59 -137 l2
-53 -170 -7 c-234 -9 -359 -38 -460 -108 -94 -64 -135 -157 -135 -301 0 -249
141 -388 392 -389 153 0 247 36 343 131 33 32 64 59 68 59 4 0 7 -38 7 -85 l0
-85 166 0 165 0 -3 473 c-4 464 -4 473 -27 535 -45 123 -162 216 -310 248 -81
17 -286 17 -397 -1z m366 -747 c0 -124 -37 -194 -124 -234 -89 -41 -190 -30
-241 26 -28 31 -28 137 1 176 50 67 122 92 272 93 l92 1 0 -62z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Loading…
Cancel
Save