ilhamdev commited on
Commit
39754cf
·
1 Parent(s): 5862716

feat: optimize UI, SEO metadata for Google Search Console, and add reprocess fallback models

Browse files
app.py CHANGED
@@ -89,6 +89,8 @@ with gr.Blocks(title="Background Remover AI") as demo:
89
  "birefnet-portrait",
90
  "birefnet-general",
91
  "isnet-general-use",
 
 
92
  "u2net_human_seg",
93
  ],
94
  value="birefnet-portrait",
 
89
  "birefnet-portrait",
90
  "birefnet-general",
91
  "isnet-general-use",
92
+ "isnet-anime",
93
+ "u2net",
94
  "u2net_human_seg",
95
  ],
96
  value="birefnet-portrait",
frontend/.env.example CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  # URL Backend Hugging Face ZeroGPU (Default: https://ilhamdev-rembg.hf.space)
2
  NEXT_PUBLIC_HF_SPACE_URL=https://ilhamdev-rembg.hf.space
3
 
 
1
+ # Domain URL Website (Canonical URL & Google Search Console)
2
+ NEXT_PUBLIC_SITE_URL=https://rembg.vercel.app
3
+
4
  # URL Backend Hugging Face ZeroGPU (Default: https://ilhamdev-rembg.hf.space)
5
  NEXT_PUBLIC_HF_SPACE_URL=https://ilhamdev-rembg.hf.space
6
 
frontend/src/app/globals.css CHANGED
@@ -1,35 +1,38 @@
1
- /* ===== GLOBAL CSS — RemBG Frontend ===== */
2
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
3
 
4
  :root {
5
- /* Color palette — deep dark with violet accent */
6
  --bg-base: #09090b;
7
- --bg-surface: #111113;
8
  --bg-card: #18181b;
9
- --bg-card-hover: #1f1f23;
10
- --border: rgba(255, 255, 255, 0.08);
11
- --border-accent: rgba(139, 92, 246, 0.4);
12
 
13
- --text-primary: #fafafa;
 
 
 
 
 
14
  --text-secondary: #a1a1aa;
15
- --text-muted: #52525b;
16
 
17
- --accent: #8b5cf6;
18
- --accent-light: #a78bfa;
19
- --accent-dark: #7c3aed;
20
- --accent-glow: rgba(139, 92, 246, 0.25);
21
 
22
- --success: #22c55e;
23
- --error: #ef4444;
24
- --warning: #f59e0b;
25
 
26
- --radius-sm: 8px;
27
- --radius-md: 12px;
28
- --radius-lg: 16px;
29
- --radius-xl: 24px;
 
30
 
31
- --shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
32
- --shadow-glow: 0 0 40px rgba(139, 92, 246, 0.15);
33
  }
34
 
35
  *, *::before, *::after {
@@ -40,41 +43,49 @@
40
 
41
  html {
42
  scroll-behavior: smooth;
 
43
  }
44
 
45
  body {
46
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
47
  background-color: var(--bg-base);
48
  color: var(--text-primary);
49
  min-height: 100vh;
50
  line-height: 1.6;
51
  -webkit-font-smoothing: antialiased;
 
 
 
 
 
 
 
52
  }
53
 
54
- /* ===== LAYOUT ===== */
55
  .app-wrapper {
56
  min-height: 100vh;
57
- background:
58
- radial-gradient(ellipse 80% 60% at 50% -20%, rgba(139, 92, 246, 0.12) 0%, transparent 70%),
59
- radial-gradient(ellipse 60% 40% at 80% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 60%),
60
- var(--bg-base);
61
  }
62
 
63
  .container {
64
- max-width: 1100px;
 
65
  margin: 0 auto;
66
- padding: 0 24px;
67
  }
68
 
69
  /* ===== HEADER ===== */
70
  .header {
71
- padding: 24px 0;
72
- border-bottom: 1px solid var(--border);
73
- backdrop-filter: blur(12px);
74
- background: rgba(9, 9, 11, 0.8);
75
  position: sticky;
76
  top: 0;
77
- z-index: 100;
78
  }
79
 
80
  .header-inner {
@@ -84,358 +95,380 @@ body {
84
  }
85
 
86
  .logo {
87
- display: flex;
88
  align-items: center;
89
  gap: 10px;
90
  text-decoration: none;
 
91
  }
92
 
93
- .logo-icon {
94
- width: 36px;
95
- height: 36px;
96
- background: linear-gradient(135deg, var(--accent), #3b82f6);
97
- border-radius: var(--radius-sm);
98
- display: flex;
99
  align-items: center;
100
  justify-content: center;
101
- font-size: 18px;
102
- box-shadow: 0 0 20px var(--accent-glow);
 
 
 
 
103
  }
104
 
105
  .logo-text {
106
- font-size: 20px;
107
  font-weight: 700;
108
- background: linear-gradient(135deg, var(--text-primary), var(--accent-light));
109
- -webkit-background-clip: text;
110
- -webkit-text-fill-color: transparent;
111
- background-clip: text;
112
  }
113
 
114
- .header-badge {
115
  display: flex;
116
  align-items: center;
117
- gap: 6px;
118
- padding: 6px 12px;
119
- background: rgba(139, 92, 246, 0.1);
120
- border: 1px solid var(--border-accent);
121
- border-radius: 100px;
 
 
122
  font-size: 12px;
123
- color: var(--accent-light);
124
- font-weight: 500;
 
 
 
125
  }
126
 
127
- .badge-dot {
128
- width: 6px;
129
- height: 6px;
130
- background: var(--success);
131
  border-radius: 50%;
132
- animation: pulse 2s infinite;
 
 
 
 
 
 
 
 
 
 
133
  }
134
 
135
- @keyframes pulse {
136
- 0%, 100% { opacity: 1; transform: scale(1); }
137
- 50% { opacity: 0.5; transform: scale(0.8); }
138
  }
139
 
140
  /* ===== HERO ===== */
141
  .hero {
142
- padding: 72px 0 48px;
143
  text-align: center;
144
  }
145
 
146
- .hero-label {
147
- display: inline-flex;
148
- align-items: center;
149
- gap: 6px;
150
- padding: 6px 14px;
151
- background: rgba(139, 92, 246, 0.08);
152
- border: 1px solid var(--border-accent);
153
- border-radius: 100px;
154
- font-size: 12px;
155
- color: var(--accent-light);
156
- font-weight: 500;
157
- margin-bottom: 24px;
158
- }
159
-
160
  .hero h1 {
161
- font-size: clamp(36px, 6vw, 64px);
162
- font-weight: 800;
163
- line-height: 1.1;
164
  letter-spacing: -0.03em;
165
- margin-bottom: 20px;
166
- }
167
-
168
- .hero h1 span {
169
- background: linear-gradient(135deg, var(--accent-light) 0%, #60a5fa 100%);
170
- -webkit-background-clip: text;
171
- -webkit-text-fill-color: transparent;
172
- background-clip: text;
173
  }
174
 
175
  .hero-desc {
176
- font-size: 18px;
177
  color: var(--text-secondary);
178
- max-width: 560px;
179
- margin: 0 auto 40px;
180
- line-height: 1.7;
181
  }
182
 
183
- /* ===== MAIN CARD ===== */
184
  .main-card {
185
- background: var(--bg-card);
186
  border: 1px solid var(--border);
187
- border-radius: var(--radius-xl);
188
- padding: 32px;
189
- margin-bottom: 24px;
190
  box-shadow: var(--shadow-card);
191
- transition: border-color 0.3s ease;
192
  }
193
 
194
- .main-card:hover {
195
- border-color: rgba(139, 92, 246, 0.2);
196
- }
197
-
198
- /* ===== SETTINGS ROW ===== */
199
- .settings-row {
200
  display: grid;
201
- grid-template-columns: 1fr auto;
202
  gap: 16px;
203
- align-items: center;
204
- margin-bottom: 24px;
205
  }
206
 
207
- .field-label {
208
- font-size: 13px;
209
- font-weight: 500;
210
- color: var(--text-secondary);
211
- margin-bottom: 8px;
212
- text-transform: uppercase;
213
- letter-spacing: 0.05em;
214
  }
215
 
216
- .select-wrapper {
217
- position: relative;
 
 
 
 
218
  }
219
 
220
- .select-wrapper select {
221
  width: 100%;
222
- padding: 10px 36px 10px 14px;
223
- background: var(--bg-surface);
224
  border: 1px solid var(--border);
225
  border-radius: var(--radius-sm);
226
  color: var(--text-primary);
227
- font-size: 14px;
228
  font-family: inherit;
229
- appearance: none;
230
  cursor: pointer;
231
- transition: border-color 0.2s, box-shadow 0.2s;
232
  outline: none;
 
233
  }
234
 
235
- .select-wrapper select:focus {
236
- border-color: var(--accent);
237
- box-shadow: 0 0 0 3px var(--accent-glow);
238
  }
239
 
240
- .select-arrow {
241
- position: absolute;
242
- right: 10px;
243
- top: 50%;
244
- transform: translateY(-50%);
245
- color: var(--text-muted);
246
- pointer-events: none;
247
- }
248
-
249
- .toggle-group {
250
  display: flex;
251
  align-items: center;
252
  gap: 10px;
 
 
 
 
 
 
 
253
  }
254
 
255
- .toggle-label {
256
- font-size: 14px;
 
 
 
 
 
 
 
257
  color: var(--text-secondary);
258
- white-space: nowrap;
259
  }
260
 
261
- /* Toggle switch */
262
- .toggle {
263
- position: relative;
264
- width: 44px;
265
- height: 24px;
 
 
266
  cursor: pointer;
 
 
267
  }
268
 
269
- .toggle input {
270
- opacity: 0;
271
- width: 0;
272
- height: 0;
273
- position: absolute;
274
  }
275
 
276
- .toggle-track {
277
- position: absolute;
278
- inset: 0;
279
- background: var(--bg-surface);
280
- border: 1px solid var(--border);
281
- border-radius: 100px;
282
- transition: background 0.3s, border-color 0.3s;
283
  }
284
 
285
- .toggle input:checked + .toggle-track {
286
- background: var(--accent);
287
- border-color: var(--accent);
 
 
 
 
288
  }
289
 
290
- .toggle-thumb {
291
- position: absolute;
292
- top: 3px;
293
- left: 3px;
294
- width: 16px;
295
- height: 16px;
296
- background: white;
297
- border-radius: 50%;
298
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
299
- box-shadow: 0 1px 4px rgba(0,0,0,0.4);
300
  }
301
 
302
- .toggle input:checked ~ .toggle-thumb {
303
- transform: translateX(20px);
 
304
  }
305
 
306
- /* ===== UPLOAD ZONE ===== */
307
- .upload-zone {
308
- border: 2px dashed var(--border);
309
- border-radius: var(--radius-lg);
310
- padding: 48px 24px;
311
- text-align: center;
312
- cursor: pointer;
313
- transition: all 0.3s ease;
314
- position: relative;
315
- background: var(--bg-surface);
316
- overflow: hidden;
317
  }
318
 
319
- .upload-zone::before {
320
- content: '';
321
- position: absolute;
322
- inset: 0;
323
- background: radial-gradient(ellipse at center, rgba(139, 92, 246, 0.05) 0%, transparent 70%);
324
- opacity: 0;
325
- transition: opacity 0.3s;
 
 
 
 
326
  }
327
 
328
- .upload-zone:hover::before,
329
- .upload-zone.dragover::before {
330
- opacity: 1;
 
 
331
  }
332
 
333
- .upload-zone:hover,
334
- .upload-zone.dragover {
335
- border-color: var(--accent);
336
- background: rgba(139, 92, 246, 0.04);
337
- transform: scale(1.005);
338
  }
339
 
340
- .upload-zone.has-image {
341
- border-style: solid;
342
- border-color: var(--border-accent);
343
- padding: 0;
 
344
  }
345
 
346
- .upload-icon {
347
- font-size: 48px;
348
- margin-bottom: 16px;
349
- display: block;
350
- filter: drop-shadow(0 0 20px rgba(139, 92, 246, 0.4));
 
 
 
 
 
351
  }
352
 
353
- .upload-title {
354
- font-size: 18px;
355
- font-weight: 600;
356
  color: var(--text-primary);
357
- margin-bottom: 8px;
358
- }
359
-
360
- .upload-sub {
361
- font-size: 14px;
362
- color: var(--text-muted);
363
  }
364
 
365
- .upload-sub span {
366
- color: var(--accent-light);
 
 
 
 
 
367
  }
368
 
369
- #file-input {
370
- display: none;
 
 
371
  }
372
 
373
- /* Preview image */
374
- .preview-image {
375
- width: 100%;
376
- max-height: 400px;
377
- object-fit: contain;
378
- border-radius: calc(var(--radius-lg) - 2px);
379
- display: block;
 
 
 
 
 
380
  }
381
 
382
- /* ===== REMOVE BUTTON ===== */
383
- .btn-remove {
384
- width: 100%;
385
- padding: 16px 32px;
386
- background: linear-gradient(135deg, var(--accent), var(--accent-dark));
387
- border: none;
388
- border-radius: var(--radius-md);
389
- color: white;
390
- font-size: 16px;
391
- font-weight: 600;
392
- font-family: inherit;
393
- cursor: pointer;
394
  display: flex;
 
395
  align-items: center;
396
  justify-content: center;
397
- gap: 10px;
398
- transition: all 0.3s ease;
399
- position: relative;
400
- overflow: hidden;
401
- box-shadow: 0 4px 24px rgba(139, 92, 246, 0.3);
402
  }
403
 
404
- .btn-remove::before {
405
- content: '';
406
- position: absolute;
407
- inset: 0;
408
- background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
409
- opacity: 0;
410
- transition: opacity 0.3s;
411
  }
412
 
413
- .btn-remove:hover:not(:disabled)::before {
414
- opacity: 1;
 
 
 
415
  }
416
 
417
- .btn-remove:hover:not(:disabled) {
418
- transform: translateY(-2px);
419
- box-shadow: 0 8px 32px rgba(139, 92, 246, 0.45);
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  }
421
 
422
- .btn-remove:active:not(:disabled) {
423
- transform: translateY(0);
424
  }
425
 
426
- .btn-remove:disabled {
427
- opacity: 0.5;
428
  cursor: not-allowed;
429
  }
430
 
431
- /* Loading spinner */
432
- .spinner {
433
- width: 20px;
434
- height: 20px;
435
- border: 2px solid rgba(255,255,255,0.3);
436
- border-top-color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  border-radius: 50%;
438
- animation: spin 0.8s linear infinite;
439
  }
440
 
441
  @keyframes spin {
@@ -445,248 +478,260 @@ body {
445
  /* ===== RESULT SECTION ===== */
446
  .result-section {
447
  margin-top: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  }
449
 
450
- .section-title {
451
  font-size: 14px;
452
  font-weight: 600;
453
- color: var(--text-secondary);
454
- text-transform: uppercase;
455
- letter-spacing: 0.05em;
456
- margin-bottom: 16px;
 
457
  display: flex;
458
  align-items: center;
459
- gap: 8px;
 
 
 
 
460
  }
461
 
462
- .result-card {
463
- background: var(--bg-card);
464
- border: 1px solid var(--border);
465
- border-radius: var(--radius-xl);
466
- overflow: hidden;
467
- box-shadow: var(--shadow-card);
 
 
 
 
468
  }
469
 
470
- /* Checkerboard background untuk transparan */
 
 
 
 
 
 
 
471
  .result-canvas {
472
- background-image:
473
- linear-gradient(45deg, #2a2a2e 25%, transparent 25%),
474
- linear-gradient(-45deg, #2a2a2e 25%, transparent 25%),
475
- linear-gradient(45deg, transparent 75%, #2a2a2e 75%),
476
- linear-gradient(-45deg, transparent 75%, #2a2a2e 75%);
477
- background-size: 20px 20px;
478
- background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
479
- background-color: #1e1e22;
480
  padding: 16px;
481
  display: flex;
482
  align-items: center;
483
  justify-content: center;
484
- min-height: 300px;
 
 
 
485
  }
486
 
487
- .result-img {
488
- max-width: 100%;
489
- max-height: 500px;
490
- object-fit: contain;
491
- border-radius: var(--radius-md);
492
- animation: fadeIn 0.5s ease;
 
 
 
493
  }
494
 
495
- @keyframes fadeIn {
496
- from { opacity: 0; transform: scale(0.97); }
497
- to { opacity: 1; transform: scale(1); }
498
  }
499
 
500
- .result-actions {
501
- padding: 16px 20px;
502
- border-top: 1px solid var(--border);
503
- display: flex;
504
- gap: 12px;
505
- background: var(--bg-surface);
506
  }
507
 
508
- .btn-download {
509
- flex: 1;
510
- padding: 12px;
511
- background: linear-gradient(135deg, var(--accent), var(--accent-dark));
512
- border: none;
513
- border-radius: var(--radius-sm);
514
- color: white;
515
- font-size: 14px;
516
- font-weight: 600;
517
- font-family: inherit;
518
- cursor: pointer;
519
  display: flex;
520
- align-items: center;
521
- justify-content: center;
522
- gap: 8px;
523
- text-decoration: none;
524
- transition: all 0.2s ease;
525
- box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
526
  }
527
 
528
- .btn-download:hover {
529
- transform: translateY(-1px);
530
- box-shadow: 0 6px 24px rgba(139, 92, 246, 0.4);
 
 
 
 
531
  }
532
 
533
- .btn-reset {
534
- padding: 12px 20px;
535
- background: transparent;
536
- border: 1px solid var(--border);
537
- border-radius: var(--radius-sm);
538
- color: var(--text-secondary);
539
- font-size: 14px;
540
- font-weight: 500;
541
- font-family: inherit;
542
- cursor: pointer;
543
  display: flex;
544
- align-items: center;
545
- gap: 8px;
546
- transition: all 0.2s ease;
547
  }
548
 
549
- .btn-reset:hover {
550
- border-color: var(--border-accent);
551
- color: var(--accent-light);
552
- background: rgba(139, 92, 246, 0.05);
553
  }
554
 
555
- /* ===== ERROR MESSAGE ===== */
556
- .error-msg {
557
- margin-top: 12px;
558
- padding: 12px 16px;
559
- background: rgba(239, 68, 68, 0.08);
560
- border: 1px solid rgba(239, 68, 68, 0.25);
561
- border-radius: var(--radius-sm);
562
- color: #fca5a5;
563
- font-size: 14px;
564
- display: flex;
565
- align-items: flex-start;
566
- gap: 8px;
567
- animation: fadeIn 0.3s ease;
568
  }
569
 
570
- /* ===== FEATURES GRID ===== */
571
- .features {
572
- padding: 64px 0;
 
 
573
  }
574
 
575
- .features-grid {
576
- display: grid;
577
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
578
- gap: 16px;
579
  }
580
 
581
- .feature-card {
582
- padding: 24px;
583
- background: var(--bg-card);
584
- border: 1px solid var(--border);
585
- border-radius: var(--radius-lg);
586
- transition: all 0.3s ease;
587
  }
588
 
589
- .feature-card:hover {
590
- border-color: var(--border-accent);
591
- transform: translateY(-2px);
592
- box-shadow: 0 8px 32px rgba(0,0,0,0.3), var(--shadow-glow);
593
  }
594
 
595
- .feature-icon {
596
- font-size: 28px;
597
- margin-bottom: 16px;
598
- display: block;
 
599
  }
600
 
601
- .feature-title {
602
- font-size: 16px;
603
  font-weight: 600;
604
- margin-bottom: 8px;
605
  color: var(--text-primary);
 
 
606
  }
607
 
608
- .feature-desc {
609
- font-size: 14px;
610
  color: var(--text-secondary);
611
- line-height: 1.6;
612
  }
613
 
614
- /* ===== FOOTER ===== */
615
- .footer {
616
- padding: 32px 0;
617
- border-top: 1px solid var(--border);
618
- text-align: center;
 
619
  }
620
 
621
- .footer-text {
622
- font-size: 14px;
623
- color: var(--text-muted);
 
624
  }
625
 
626
- .footer-text a {
627
- color: var(--accent-light);
628
- text-decoration: none;
629
- transition: color 0.2s;
630
  }
631
 
632
- .footer-text a:hover {
633
- color: var(--accent);
 
 
 
634
  }
635
 
636
- /* ===== PROGRESS BAR ===== */
637
- .progress-bar {
638
- height: 3px;
639
- background: var(--bg-surface);
640
- border-radius: 100px;
641
- margin-top: 16px;
642
- overflow: hidden;
643
  }
644
 
645
- .progress-fill {
646
- height: 100%;
647
- background: linear-gradient(90deg, var(--accent), #60a5fa);
648
- border-radius: 100px;
649
- width: 0%;
650
- transition: width 0.5s ease;
651
- animation: shimmer 1.5s infinite;
652
  }
653
 
654
- @keyframes shimmer {
655
- 0% { background-position: -200px 0; }
656
- 100% { background-position: 200px 0; }
657
  }
658
 
659
- /* ===== CLOUDFLARE TURNSTILE ===== */
660
- .turnstile-wrapper {
661
- margin-top: 18px;
662
- display: flex;
663
- flex-direction: column;
664
- align-items: center;
665
- justify-content: center;
666
- gap: 8px;
667
- min-height: 65px;
668
  }
669
 
670
- .turnstile-hint {
671
- font-size: 0.78rem;
672
- color: var(--text-secondary);
673
- display: flex;
674
- align-items: center;
675
- gap: 5px;
676
  }
677
 
678
- /* ===== RESPONSIVE ===== */
679
- @media (max-width: 640px) {
680
- .settings-row {
 
 
 
 
681
  grid-template-columns: 1fr;
682
  }
 
683
  .hero {
684
- padding: 48px 0 32px;
685
  }
 
686
  .main-card {
687
- padding: 20px;
688
  }
689
- .result-actions {
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  flex-direction: column;
 
691
  }
692
  }
 
1
+ /* ===== GLOBAL STYLES: STUDIO DARK MINIMALIST ===== */
 
2
 
3
  :root {
4
+ /* Neutral Studio Dark Palette */
5
  --bg-base: #09090b;
6
+ --bg-surface: #121215;
7
  --bg-card: #18181b;
8
+ --bg-card-subtle: #202024;
9
+ --bg-input: #141418;
 
10
 
11
+ --border: rgba(255, 255, 255, 0.10);
12
+ --border-subtle: rgba(255, 255, 255, 0.06);
13
+ --border-strong: rgba(255, 255, 255, 0.20);
14
+ --border-focus: #e4e4e7;
15
+
16
+ --text-primary: #f4f4f5;
17
  --text-secondary: #a1a1aa;
18
+ --text-muted: #71717a;
19
 
20
+ --accent-cta-bg: #fafafa;
21
+ --accent-cta-text: #09090b;
22
+ --accent-cta-hover: #ffffff;
 
23
 
24
+ --status-success: #22c55e;
25
+ --status-warning: #f59e0b;
26
+ --status-error: #ef4444;
27
 
28
+ --radius-xs: 4px;
29
+ --radius-sm: 6px;
30
+ --radius-md: 10px;
31
+ --radius-lg: 14px;
32
+ --radius-xl: 18px;
33
 
34
+ --shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.3);
35
+ --shadow-card: 0 8px 30px rgba(0, 0, 0, 0.45);
36
  }
37
 
38
  *, *::before, *::after {
 
43
 
44
  html {
45
  scroll-behavior: smooth;
46
+ color-scheme: dark;
47
  }
48
 
49
  body {
50
+ font-family: inherit;
51
  background-color: var(--bg-base);
52
  color: var(--text-primary);
53
  min-height: 100vh;
54
  line-height: 1.6;
55
  -webkit-font-smoothing: antialiased;
56
+ -moz-osx-font-smoothing: grayscale;
57
+ }
58
+
59
+ /* Accessible focus visible ring */
60
+ :focus-visible {
61
+ outline: 2px solid var(--border-focus);
62
+ outline-offset: 2px;
63
  }
64
 
65
+ /* ===== APP LAYOUT ===== */
66
  .app-wrapper {
67
  min-height: 100vh;
68
+ display: flex;
69
+ flex-direction: column;
70
+ background-color: var(--bg-base);
 
71
  }
72
 
73
  .container {
74
+ width: 100%;
75
+ max-width: 980px;
76
  margin: 0 auto;
77
+ padding: 0 20px;
78
  }
79
 
80
  /* ===== HEADER ===== */
81
  .header {
82
+ padding: 16px 0;
83
+ border-bottom: 1px solid var(--border-subtle);
84
+ background-color: rgba(9, 9, 11, 0.85);
85
+ backdrop-filter: blur(10px);
86
  position: sticky;
87
  top: 0;
88
+ z-index: 50;
89
  }
90
 
91
  .header-inner {
 
95
  }
96
 
97
  .logo {
98
+ display: inline-flex;
99
  align-items: center;
100
  gap: 10px;
101
  text-decoration: none;
102
+ color: var(--text-primary);
103
  }
104
 
105
+ .logo-badge {
106
+ display: inline-flex;
 
 
 
 
107
  align-items: center;
108
  justify-content: center;
109
+ width: 32px;
110
+ height: 32px;
111
+ border-radius: var(--radius-sm);
112
+ background-color: var(--bg-card);
113
+ border: 1px solid var(--border);
114
+ color: var(--text-primary);
115
  }
116
 
117
  .logo-text {
118
+ font-size: 17px;
119
  font-weight: 700;
120
+ letter-spacing: -0.02em;
 
 
 
121
  }
122
 
123
+ .header-meta {
124
  display: flex;
125
  align-items: center;
126
+ gap: 12px;
127
+ }
128
+
129
+ .system-status {
130
+ display: inline-flex;
131
+ align-items: center;
132
+ gap: 7px;
133
  font-size: 12px;
134
+ color: var(--text-secondary);
135
+ padding: 5px 10px;
136
+ border-radius: var(--radius-sm);
137
+ background-color: var(--bg-surface);
138
+ border: 1px solid var(--border-subtle);
139
  }
140
 
141
+ .status-dot {
142
+ width: 7px;
143
+ height: 7px;
 
144
  border-radius: 50%;
145
+ background-color: var(--status-success);
146
+ }
147
+
148
+ .hf-link {
149
+ font-size: 12px;
150
+ color: var(--text-muted);
151
+ text-decoration: none;
152
+ display: inline-flex;
153
+ align-items: center;
154
+ gap: 4px;
155
+ transition: color 0.15s ease;
156
  }
157
 
158
+ .hf-link:hover {
159
+ color: var(--text-primary);
 
160
  }
161
 
162
  /* ===== HERO ===== */
163
  .hero {
164
+ padding: 48px 0 32px;
165
  text-align: center;
166
  }
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  .hero h1 {
169
+ font-size: clamp(28px, 4.5vw, 44px);
170
+ font-weight: 700;
171
+ line-height: 1.2;
172
  letter-spacing: -0.03em;
173
+ color: var(--text-primary);
174
+ margin-bottom: 12px;
 
 
 
 
 
 
175
  }
176
 
177
  .hero-desc {
178
+ font-size: 15px;
179
  color: var(--text-secondary);
180
+ max-width: 580px;
181
+ margin: 0 auto;
182
+ line-height: 1.6;
183
  }
184
 
185
+ /* ===== MAIN TOOL CARD ===== */
186
  .main-card {
187
+ background-color: var(--bg-card);
188
  border: 1px solid var(--border);
189
+ border-radius: var(--radius-lg);
190
+ padding: 24px;
 
191
  box-shadow: var(--shadow-card);
 
192
  }
193
 
194
+ /* Configuration grid */
195
+ .config-grid {
 
 
 
 
196
  display: grid;
197
+ grid-template-columns: 1fr 220px;
198
  gap: 16px;
199
+ margin-bottom: 20px;
 
200
  }
201
 
202
+ .field-group {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 6px;
 
 
 
206
  }
207
 
208
+ .field-label {
209
+ font-size: 12px;
210
+ font-weight: 600;
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.04em;
213
+ color: var(--text-secondary);
214
  }
215
 
216
+ .select-control {
217
  width: 100%;
218
+ padding: 10px 12px;
219
+ background-color: var(--bg-input);
220
  border: 1px solid var(--border);
221
  border-radius: var(--radius-sm);
222
  color: var(--text-primary);
223
+ font-size: 13px;
224
  font-family: inherit;
 
225
  cursor: pointer;
 
226
  outline: none;
227
+ transition: border-color 0.15s ease;
228
  }
229
 
230
+ .select-control:focus {
231
+ border-color: var(--border-focus);
 
232
  }
233
 
234
+ .toggle-card {
 
 
 
 
 
 
 
 
 
235
  display: flex;
236
  align-items: center;
237
  gap: 10px;
238
+ padding: 9px 12px;
239
+ background-color: var(--bg-input);
240
+ border: 1px solid var(--border);
241
+ border-radius: var(--radius-sm);
242
+ cursor: pointer;
243
+ user-select: none;
244
+ min-height: 42px;
245
  }
246
 
247
+ .toggle-card input[type="checkbox"] {
248
+ width: 16px;
249
+ height: 16px;
250
+ cursor: pointer;
251
+ accent-color: var(--accent-cta-bg);
252
+ }
253
+
254
+ .toggle-text {
255
+ font-size: 13px;
256
  color: var(--text-secondary);
257
+ line-height: 1.3;
258
  }
259
 
260
+ /* ===== DROPZONE & UPLOAD ===== */
261
+ .dropzone {
262
+ border: 1px dashed var(--border-strong);
263
+ border-radius: var(--radius-md);
264
+ background-color: var(--bg-surface);
265
+ padding: 40px 20px;
266
+ text-align: center;
267
  cursor: pointer;
268
+ position: relative;
269
+ transition: border-color 0.15s ease, background-color 0.15s ease;
270
  }
271
 
272
+ .dropzone:hover,
273
+ .dropzone.drag-active {
274
+ border-color: var(--border-focus);
275
+ background-color: var(--bg-card-subtle);
 
276
  }
277
 
278
+ .dropzone-inner {
279
+ display: flex;
280
+ flex-direction: column;
281
+ align-items: center;
282
+ gap: 10px;
 
 
283
  }
284
 
285
+ .dropzone-icon {
286
+ width: 44px;
287
+ height: 44px;
288
+ color: var(--text-secondary);
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
  }
293
 
294
+ .dropzone-title {
295
+ font-size: 15px;
296
+ font-weight: 600;
297
+ color: var(--text-primary);
 
 
 
 
 
 
298
  }
299
 
300
+ .dropzone-hint {
301
+ font-size: 13px;
302
+ color: var(--text-muted);
303
  }
304
 
305
+ /* Uploaded Image Preview */
306
+ .preview-container {
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 12px;
 
 
 
 
 
 
310
  }
311
 
312
+ .preview-media-box {
313
+ background-color: var(--bg-input);
314
+ border: 1px solid var(--border);
315
+ border-radius: var(--radius-md);
316
+ padding: 16px;
317
+ display: flex;
318
+ justify-content: center;
319
+ align-items: center;
320
+ min-height: 240px;
321
+ max-height: 420px;
322
+ overflow: hidden;
323
  }
324
 
325
+ .preview-img {
326
+ max-width: 100%;
327
+ max-height: 380px;
328
+ object-fit: contain;
329
+ border-radius: var(--radius-sm);
330
  }
331
 
332
+ .preview-meta-row {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: space-between;
336
+ padding: 0 4px;
337
  }
338
 
339
+ .meta-info {
340
+ font-size: 12px;
341
+ color: var(--text-muted);
342
+ display: flex;
343
+ gap: 8px;
344
  }
345
 
346
+ .btn-ghost-small {
347
+ background: transparent;
348
+ border: 1px solid var(--border);
349
+ border-radius: var(--radius-sm);
350
+ color: var(--text-secondary);
351
+ font-size: 12px;
352
+ padding: 6px 12px;
353
+ cursor: pointer;
354
+ transition: all 0.15s ease;
355
+ min-height: 32px;
356
  }
357
 
358
+ .btn-ghost-small:hover {
 
 
359
  color: var(--text-primary);
360
+ border-color: var(--border-strong);
 
 
 
 
 
361
  }
362
 
363
+ /* Progress bar */
364
+ .progress-track {
365
+ height: 4px;
366
+ background-color: var(--bg-surface);
367
+ border-radius: var(--radius-xs);
368
+ margin-top: 16px;
369
+ overflow: hidden;
370
  }
371
 
372
+ .progress-bar-fill {
373
+ height: 100%;
374
+ background-color: var(--accent-cta-bg);
375
+ transition: width 0.3s ease;
376
  }
377
 
378
+ /* Error message */
379
+ .error-banner {
380
+ margin-top: 14px;
381
+ padding: 12px 14px;
382
+ background-color: rgba(239, 68, 68, 0.10);
383
+ border: 1px solid rgba(239, 68, 68, 0.30);
384
+ border-radius: var(--radius-sm);
385
+ color: #fca5a5;
386
+ font-size: 13px;
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 10px;
390
  }
391
 
392
+ /* Turnstile container */
393
+ .turnstile-box {
394
+ margin-top: 20px;
 
 
 
 
 
 
 
 
 
395
  display: flex;
396
+ flex-direction: column;
397
  align-items: center;
398
  justify-content: center;
399
+ gap: 6px;
400
+ min-height: 65px;
 
 
 
401
  }
402
 
403
+ .turnstile-notice {
404
+ font-size: 11px;
405
+ color: var(--text-muted);
 
 
 
 
406
  }
407
 
408
+ /* Action button area */
409
+ .action-row {
410
+ margin-top: 20px;
411
+ display: flex;
412
+ gap: 12px;
413
  }
414
 
415
+ .btn-primary {
416
+ flex: 1;
417
+ display: inline-flex;
418
+ align-items: center;
419
+ justify-content: center;
420
+ gap: 8px;
421
+ background-color: var(--accent-cta-bg);
422
+ color: var(--accent-cta-text);
423
+ border: none;
424
+ border-radius: var(--radius-sm);
425
+ font-size: 14px;
426
+ font-weight: 600;
427
+ padding: 12px 20px;
428
+ cursor: pointer;
429
+ transition: background-color 0.15s ease, opacity 0.15s ease;
430
+ min-height: 44px;
431
  }
432
 
433
+ .btn-primary:hover:not(:disabled) {
434
+ background-color: var(--accent-cta-hover);
435
  }
436
 
437
+ .btn-primary:disabled {
438
+ opacity: 0.45;
439
  cursor: not-allowed;
440
  }
441
 
442
+ .btn-secondary {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ justify-content: center;
446
+ gap: 6px;
447
+ background-color: transparent;
448
+ color: var(--text-secondary);
449
+ border: 1px solid var(--border);
450
+ border-radius: var(--radius-sm);
451
+ font-size: 13px;
452
+ font-weight: 500;
453
+ padding: 10px 16px;
454
+ cursor: pointer;
455
+ transition: all 0.15s ease;
456
+ min-height: 44px;
457
+ }
458
+
459
+ .btn-secondary:hover:not(:disabled) {
460
+ color: var(--text-primary);
461
+ border-color: var(--border-strong);
462
+ }
463
+
464
+ /* Minimalist loading spinner */
465
+ .spinner-icon {
466
+ width: 16px;
467
+ height: 16px;
468
+ border: 2px solid rgba(9, 9, 11, 0.25);
469
+ border-top-color: #09090b;
470
  border-radius: 50%;
471
+ animation: spin 0.6s linear infinite;
472
  }
473
 
474
  @keyframes spin {
 
478
  /* ===== RESULT SECTION ===== */
479
  .result-section {
480
  margin-top: 24px;
481
+ background-color: var(--bg-card);
482
+ border: 1px solid var(--border);
483
+ border-radius: var(--radius-lg);
484
+ padding: 24px;
485
+ box-shadow: var(--shadow-card);
486
+ }
487
+
488
+ .result-header {
489
+ display: flex;
490
+ align-items: center;
491
+ justify-content: space-between;
492
+ margin-bottom: 16px;
493
+ flex-wrap: wrap;
494
+ gap: 12px;
495
  }
496
 
497
+ .result-title {
498
  font-size: 14px;
499
  font-weight: 600;
500
+ color: var(--text-primary);
501
+ }
502
+
503
+ /* Preview background inspector toggle */
504
+ .bg-inspector-group {
505
  display: flex;
506
  align-items: center;
507
+ gap: 4px;
508
+ background-color: var(--bg-input);
509
+ padding: 3px;
510
+ border-radius: var(--radius-sm);
511
+ border: 1px solid var(--border-subtle);
512
  }
513
 
514
+ .bg-inspector-btn {
515
+ background: transparent;
516
+ border: none;
517
+ font-size: 11px;
518
+ color: var(--text-muted);
519
+ padding: 5px 9px;
520
+ border-radius: var(--radius-xs);
521
+ cursor: pointer;
522
+ transition: all 0.15s ease;
523
+ min-height: 28px;
524
  }
525
 
526
+ .bg-inspector-btn.active {
527
+ background-color: var(--bg-card);
528
+ color: var(--text-primary);
529
+ font-weight: 600;
530
+ box-shadow: var(--shadow-subtle);
531
+ }
532
+
533
+ /* Canvas backgrounds */
534
  .result-canvas {
535
+ border: 1px solid var(--border);
536
+ border-radius: var(--radius-md);
 
 
 
 
 
 
537
  padding: 16px;
538
  display: flex;
539
  align-items: center;
540
  justify-content: center;
541
+ min-height: 320px;
542
+ max-height: 520px;
543
+ overflow: hidden;
544
+ transition: background-color 0.2s ease;
545
  }
546
 
547
+ .result-canvas.checkerboard {
548
+ background-image:
549
+ linear-gradient(45deg, #232328 25%, transparent 25%),
550
+ linear-gradient(-45deg, #232328 25%, transparent 25%),
551
+ linear-gradient(45deg, transparent 75%, #232328 75%),
552
+ linear-gradient(-45deg, transparent 75%, #232328 75%);
553
+ background-size: 16px 16px;
554
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
555
+ background-color: #17171b;
556
  }
557
 
558
+ .result-canvas.white {
559
+ background-color: #ffffff;
 
560
  }
561
 
562
+ .result-canvas.dark {
563
+ background-color: #09090b;
 
 
 
 
564
  }
565
 
566
+ .result-image {
567
+ max-width: 100%;
568
+ max-height: 480px;
569
+ object-fit: contain;
570
+ }
571
+
572
+ .result-footer-actions {
573
+ margin-top: 18px;
 
 
 
574
  display: flex;
575
+ gap: 12px;
 
 
 
 
 
576
  }
577
 
578
+ /* Reprocess panel in result card */
579
+ .reprocess-panel {
580
+ margin-top: 20px;
581
+ padding: 16px;
582
+ background-color: var(--bg-surface);
583
+ border: 1px solid var(--border-subtle);
584
+ border-radius: var(--radius-md);
585
  }
586
 
587
+ .reprocess-header {
 
 
 
 
 
 
 
 
 
588
  display: flex;
589
+ flex-direction: column;
590
+ gap: 4px;
591
+ margin-bottom: 12px;
592
  }
593
 
594
+ .reprocess-title {
595
+ font-size: 13px;
596
+ font-weight: 600;
597
+ color: var(--text-primary);
598
  }
599
 
600
+ .reprocess-desc {
601
+ font-size: 12px;
602
+ color: var(--text-muted);
 
 
 
 
 
 
 
 
 
 
603
  }
604
 
605
+ .reprocess-controls {
606
+ display: flex;
607
+ align-items: center;
608
+ gap: 10px;
609
+ flex-wrap: wrap;
610
  }
611
 
612
+ .reprocess-field {
613
+ flex: 1;
614
+ min-width: 220px;
 
615
  }
616
 
617
+ /* ===== TECHNICAL / PROJECT INFORMATION ===== */
618
+ .info-section {
619
+ padding: 48px 0 24px;
 
 
 
620
  }
621
 
622
+ .info-grid {
623
+ display: grid;
624
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
625
+ gap: 16px;
626
  }
627
 
628
+ .info-card {
629
+ background-color: var(--bg-surface);
630
+ border: 1px solid var(--border-subtle);
631
+ border-radius: var(--radius-md);
632
+ padding: 20px;
633
  }
634
 
635
+ .info-card h2 {
636
+ font-size: 14px;
637
  font-weight: 600;
 
638
  color: var(--text-primary);
639
+ margin-bottom: 8px;
640
+ letter-spacing: -0.01em;
641
  }
642
 
643
+ .info-card p {
644
+ font-size: 13px;
645
  color: var(--text-secondary);
646
+ line-height: 1.5;
647
  }
648
 
649
+ .model-list {
650
+ list-style: none;
651
+ display: flex;
652
+ flex-direction: column;
653
+ gap: 8px;
654
+ margin-top: 8px;
655
  }
656
 
657
+ .model-list li {
658
+ font-size: 12px;
659
+ color: var(--text-secondary);
660
+ line-height: 1.4;
661
  }
662
 
663
+ .model-list strong {
664
+ color: var(--text-primary);
 
 
665
  }
666
 
667
+ /* ===== FOOTER ===== */
668
+ .footer {
669
+ margin-top: auto;
670
+ padding: 32px 0 24px;
671
+ border-top: 1px solid var(--border-subtle);
672
  }
673
 
674
+ .footer-content {
675
+ display: flex;
676
+ align-items: center;
677
+ justify-content: space-between;
678
+ flex-wrap: wrap;
679
+ gap: 12px;
 
680
  }
681
 
682
+ .footer-text {
683
+ font-size: 12px;
684
+ color: var(--text-muted);
 
 
 
 
685
  }
686
 
687
+ .footer-links {
688
+ display: flex;
689
+ gap: 16px;
690
  }
691
 
692
+ .footer-link {
693
+ font-size: 12px;
694
+ color: var(--text-muted);
695
+ text-decoration: none;
696
+ transition: color 0.15s ease;
 
 
 
 
697
  }
698
 
699
+ .footer-link:hover {
700
+ color: var(--text-primary);
 
 
 
 
701
  }
702
 
703
+ /* ===== RESPONSIVE BREAKPOINTS ===== */
704
+ @media (max-width: 720px) {
705
+ .hf-link {
706
+ display: none;
707
+ }
708
+
709
+ .config-grid {
710
  grid-template-columns: 1fr;
711
  }
712
+
713
  .hero {
714
+ padding: 32px 0 24px;
715
  }
716
+
717
  .main-card {
718
+ padding: 16px;
719
  }
720
+
721
+ .result-section {
722
+ padding: 16px;
723
+ }
724
+
725
+ .result-footer-actions {
726
+ flex-direction: column;
727
+ }
728
+
729
+ .btn-primary, .btn-secondary {
730
+ width: 100%;
731
+ }
732
+
733
+ .footer-content {
734
  flex-direction: column;
735
+ align-items: flex-start;
736
  }
737
  }
frontend/src/app/icon.svg ADDED
frontend/src/app/layout.js CHANGED
@@ -1,22 +1,116 @@
1
  import { Inter } from 'next/font/google'
2
  import './globals.css'
3
 
4
- const inter = Inter({ subsets: ['latin'] })
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  export const metadata = {
7
- title: 'RemBG — AI Background Remover',
8
- description: 'Hapus background foto secara otomatis menggunakan AI. Powered by HuggingFace ZeroGPU & rembg.',
9
- keywords: 'remove background, hapus background, AI, rembg, transparan',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  openGraph: {
11
- title: 'RemBG — AI Background Remover',
12
- description: 'Hapus background foto otomatis dengan AI. Gratis, cepat, presisi tinggi.',
 
 
 
13
  type: 'website',
14
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
 
17
  export default function RootLayout({ children }) {
18
  return (
19
  <html lang="id">
 
 
 
 
 
 
20
  <body className={inter.className}>{children}</body>
21
  </html>
22
  )
 
1
  import { Inter } from 'next/font/google'
2
  import './globals.css'
3
 
4
+ const inter = Inter({
5
+ subsets: ['latin'],
6
+ display: 'swap',
7
+ })
8
+
9
+ const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || 'https://rembg.vercel.app').replace(/\/+$/, '')
10
+
11
+ export const viewport = {
12
+ themeColor: '#09090b',
13
+ width: 'device-width',
14
+ initialScale: 1,
15
+ }
16
 
17
  export const metadata = {
18
+ metadataBase: new URL(SITE_URL),
19
+ title: 'RemBG: Hapus Background Foto AI Otomatis & Presisi Tinggi',
20
+ description: 'Hapus background foto secara instan menggunakan AI ZeroGPU dan rembg. Mendukung foto orang, produk, dan objek umum secara gratis tanpa watermark.',
21
+ keywords: [
22
+ 'hapus background foto',
23
+ 'remove background online',
24
+ 'background transparan AI',
25
+ 'rembg online',
26
+ 'BiRefNet',
27
+ 'potong latar belakang gratis',
28
+ ],
29
+ authors: [{ name: 'RemBG' }],
30
+ creator: 'RemBG',
31
+ publisher: 'RemBG',
32
+ alternates: {
33
+ canonical: '/',
34
+ },
35
  openGraph: {
36
+ title: 'RemBG: Hapus Background Foto AI Otomatis & Presisi Tinggi',
37
+ description: 'Hapus background foto instan dengan AI ZeroGPU dan rembg. Gratis, cepat, dan transparan tanpa kompresi.',
38
+ url: SITE_URL,
39
+ siteName: 'RemBG',
40
+ locale: 'id_ID',
41
  type: 'website',
42
  },
43
+ twitter: {
44
+ card: 'summary_large_image',
45
+ title: 'RemBG: Hapus Background Foto AI Otomatis & Presisi Tinggi',
46
+ description: 'Hapus background foto instan dengan AI ZeroGPU dan rembg. Gratis, cepat, dan transparan tanpa kompresi.',
47
+ },
48
+ robots: {
49
+ index: true,
50
+ follow: true,
51
+ googleBot: {
52
+ index: true,
53
+ follow: true,
54
+ 'max-video-preview': -1,
55
+ 'max-image-preview': 'large',
56
+ 'max-snippet': -1,
57
+ },
58
+ },
59
+ }
60
+
61
+ const jsonLd = {
62
+ '@context': 'https://schema.org',
63
+ '@graph': [
64
+ {
65
+ '@type': 'WebApplication',
66
+ '@id': `${SITE_URL}/#app`,
67
+ name: 'RemBG',
68
+ url: SITE_URL,
69
+ description: 'Aplikasi web penghapus background foto otomatis berbasis AI dengan model BiRefNet dan ISNet.',
70
+ applicationCategory: 'MultimediaApplication',
71
+ operatingSystem: 'All',
72
+ browserRequirements: 'Requires JavaScript. Requires HTML5.',
73
+ offers: {
74
+ '@type': 'Offer',
75
+ price: '0',
76
+ priceCurrency: 'USD',
77
+ },
78
+ },
79
+ {
80
+ '@type': 'HowTo',
81
+ '@id': `${SITE_URL}/#howto`,
82
+ name: 'Cara Menghapus Background Foto dengan RemBG',
83
+ description: 'Langkah mudah menghapus background foto secara otomatis menggunakan AI.',
84
+ step: [
85
+ {
86
+ '@type': 'HowToStep',
87
+ name: 'Pilih atau Tarik Foto',
88
+ text: 'Unggah foto berformat JPG, PNG, atau WEBP ke area unggah.',
89
+ },
90
+ {
91
+ '@type': 'HowToStep',
92
+ name: 'Pilih Model AI yang Sesuai',
93
+ text: 'Pilih model BiRefNet Portrait untuk orang atau BiRefNet General untuk produk dan objek.',
94
+ },
95
+ {
96
+ '@type': 'HowToStep',
97
+ name: 'Unduh Hasil PNG Transparan',
98
+ text: 'Klik tombol Hapus Background, periksa hasil pratinjau, dan unduh foto PNG transparan.',
99
+ },
100
+ ],
101
+ },
102
+ ],
103
  }
104
 
105
  export default function RootLayout({ children }) {
106
  return (
107
  <html lang="id">
108
+ <head>
109
+ <script
110
+ type="application/ld+json"
111
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
112
+ />
113
+ </head>
114
  <body className={inter.className}>{children}</body>
115
  </html>
116
  )
frontend/src/app/page.js CHANGED
@@ -1,19 +1,28 @@
1
  'use client'
2
 
3
- import { useState, useRef, useCallback } from 'react'
4
  import { Turnstile } from '@marsidev/react-turnstile'
5
  import { DEFAULT_TEST_SITE_KEY } from '@/lib/turnstile'
6
- import './globals.css'
7
 
8
  const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || DEFAULT_TEST_SITE_KEY
9
 
10
  const MODELS = [
11
- { value: 'birefnet-portrait', label: '👤 BiRefNet Portrait — Terbaik untuk orang' },
12
- { value: 'birefnet-general', label: '📦 BiRefNet General — Terbaik untuk objek/produk' },
13
- { value: 'isnet-general-use', label: '⚡ ISNet General — Seimbang: cepat & bagus' },
14
- { value: 'u2net_human_seg', label: '🚀 U2Net Human — Ringan untuk orang' },
 
 
15
  ]
16
 
 
 
 
 
 
 
 
 
17
  export default function Home() {
18
  const [previewUrl, setPreviewUrl] = useState(null)
19
  const [resultUrl, setResultUrl] = useState(null)
@@ -25,13 +34,14 @@ export default function Home() {
25
  const [progress, setProgress] = useState(0)
26
  const [dragover, setDragover] = useState(false)
27
  const [turnstileToken, setTurnstileToken] = useState(null)
 
28
 
29
  const fileInputRef = useRef(null)
30
  const turnstileRef = useRef(null)
31
 
32
  const handleFile = useCallback((file) => {
33
  if (!file || !file.type.startsWith('image/')) {
34
- setError('File harus berupa gambar (JPG, PNG, WEBP)')
35
  return
36
  }
37
  if (file.size > 20 * 1024 * 1024) {
@@ -45,6 +55,23 @@ export default function Home() {
45
  setPreviewUrl(url)
46
  }, [])
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const handleDrop = useCallback((e) => {
49
  e.preventDefault()
50
  setDragover(false)
@@ -60,7 +87,9 @@ export default function Home() {
60
  const handleDragLeave = () => setDragover(false)
61
 
62
  const handleFileInput = (e) => {
63
- handleFile(e.target.files[0])
 
 
64
  }
65
 
66
  const handleRemoveBg = async () => {
@@ -76,7 +105,6 @@ export default function Home() {
76
  setResultUrl(null)
77
  setProgress(0)
78
 
79
- // Simulasi progress
80
  const progressInterval = setInterval(() => {
81
  setProgress(prev => Math.min(prev + Math.random() * 15, 85))
82
  }, 800)
@@ -99,14 +127,15 @@ export default function Home() {
99
  const data = await res.json()
100
 
101
  if (!res.ok || !data.success) {
102
- throw new Error(data.error || 'Terjadi kesalahan pada server')
103
  }
104
 
105
- // API route sekarang mengembalikan base64 langsung di data.image
106
- if (!data.image) throw new Error('Tidak ada gambar hasil dari server')
 
107
 
108
  setProgress(100)
109
- setResultUrl(data.image) // base64 data URL, langsung bisa dipakai sebagai src
110
  } catch (err) {
111
  clearInterval(progressInterval)
112
  setError(err.message)
@@ -133,7 +162,8 @@ export default function Home() {
133
  if (!resultUrl) return
134
  const a = document.createElement('a')
135
  a.href = resultUrl
136
- a.download = `rembg_${imageFile?.name?.replace(/\.[^.]+$/, '') || 'result'}.png`
 
137
  a.click()
138
  }
139
 
@@ -143,13 +173,36 @@ export default function Home() {
143
  <header className="header">
144
  <div className="container">
145
  <div className="header-inner">
146
- <a className="logo" href="/">
147
- <div className="logo-icon">✂️</div>
 
 
 
 
 
 
 
 
148
  <span className="logo-text">RemBG</span>
149
  </a>
150
- <div className="header-badge">
151
- <div className="badge-dot" />
152
- Powered by ZeroGPU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  </div>
154
  </div>
155
  </div>
@@ -158,107 +211,141 @@ export default function Home() {
158
  {/* Hero */}
159
  <section className="hero">
160
  <div className="container">
161
- <div className="hero-label">
162
- 🤖 AI Background Removal
163
- </div>
164
- <h1>
165
- Hapus Background Foto<br />
166
- <span>Secara Otomatis</span>
167
- </h1>
168
  <p className="hero-desc">
169
- Upload foto, pilih model AI terbaik, dan dapatkan hasil dengan background transparan dalam hitungan detik. Gratis, presisi tinggi, tanpa watermark.
170
  </p>
171
  </div>
172
  </section>
173
 
174
- {/* Main Tool */}
175
  <main>
176
  <div className="container">
177
  <div className="main-card">
178
- {/* Settings */}
179
- <div className="settings-row">
180
- <div>
181
- <div className="field-label">Model AI</div>
182
- <div className="select-wrapper">
183
- <select
184
- id="model-select"
185
- value={model}
186
- onChange={e => setModel(e.target.value)}
187
- disabled={loading}
188
- >
189
- {MODELS.map(m => (
190
- <option key={m.value} value={m.value}>{m.label}</option>
191
- ))}
192
- </select>
193
- <span className="select-arrow">▾</span>
194
- </div>
195
  </div>
196
- <div>
197
- <div className="field-label">Alpha Matting</div>
198
- <div className="toggle-group">
199
- <label className="toggle">
200
- <input
201
- id="alpha-toggle"
202
- type="checkbox"
203
- checked={alphaMatting}
204
- onChange={e => setAlphaMatting(e.target.checked)}
205
- disabled={loading}
206
- />
207
- <div className="toggle-track" />
208
- <div className="toggle-thumb" />
209
- </label>
210
- <span className="toggle-label">
211
- {alphaMatting ? '✅ Aktif' : 'Haluskan edge rambut'}
212
  </span>
213
- </div>
214
  </div>
215
  </div>
216
 
217
- {/* Upload Zone */}
218
  <input
219
  ref={fileInputRef}
220
  id="file-input"
221
  type="file"
222
- accept="image/*"
223
  onChange={handleFileInput}
 
224
  />
225
- <div
226
- className={`upload-zone ${dragover ? 'dragover' : ''} ${previewUrl ? 'has-image' : ''}`}
227
- onClick={() => !previewUrl && fileInputRef.current?.click()}
228
- onDrop={handleDrop}
229
- onDragOver={handleDragOver}
230
- onDragLeave={handleDragLeave}
231
- id="upload-dropzone"
232
- >
233
- {previewUrl ? (
234
- <img src={previewUrl} alt="Preview" className="preview-image" />
235
- ) : (
236
- <>
237
- <span className="upload-icon">🖼️</span>
238
- <div className="upload-title">Drop gambar di sini atau klik untuk upload</div>
239
- <div className="upload-sub">
240
- Mendukung <span>JPG, PNG, WEBP</span> · Maks <span>20MB</span>
 
 
 
 
 
 
 
 
 
 
241
  </div>
242
- </>
243
- )}
244
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- {/* Progress Bar */}
247
  {loading && (
248
- <div className="progress-bar">
249
- <div className="progress-fill" style={{ width: `${progress}%` }} />
250
  </div>
251
  )}
252
 
253
- {/* Error */}
254
  {error && (
255
- <div className="error-msg" id="error-message">
256
- ⚠️ {error}
 
 
 
 
 
257
  </div>
258
  )}
259
 
260
  {/* Cloudflare Turnstile */}
261
- <div className="turnstile-wrapper">
262
  <Turnstile
263
  ref={turnstileRef}
264
  siteKey={TURNSTILE_SITE_KEY}
@@ -276,105 +363,222 @@ export default function Home() {
276
  size: 'normal',
277
  }}
278
  />
279
- <div className="turnstile-hint">
280
- 🔒 Dilindungi oleh Cloudflare Turnstile anti-bot
281
- </div>
282
  </div>
283
 
284
- {/* Action Buttons */}
285
- <div style={{ marginTop: '20px', display: 'flex', gap: '12px' }}>
286
  <button
287
  id="remove-bg-btn"
288
- className="btn-remove"
289
  onClick={handleRemoveBg}
290
  disabled={!imageFile || loading || !turnstileToken}
291
  >
292
  {loading ? (
293
  <>
294
- <div className="spinner" />
295
- Memproses dengan AI...
296
  </>
297
  ) : (
298
  <>
299
- ✂️ Hapus Background
 
 
 
300
  </>
301
  )}
302
  </button>
303
  {previewUrl && (
304
  <button
305
  id="reset-btn"
306
- className="btn-reset"
307
  onClick={handleReset}
308
- style={{ minWidth: 'auto', padding: '12px 16px' }}
309
  >
310
- 🔄
311
  </button>
312
  )}
313
  </div>
314
  </div>
315
 
316
- {/* Result */}
317
  {resultUrl && (
318
  <div className="result-section" id="result-section">
319
- <div className="section-title">
320
- ✅ Hasil Background Removed
321
- </div>
322
- <div className="result-card">
323
- <div className="result-canvas">
324
- <img src={resultUrl} alt="Hasil remove background" className="result-img" id="result-image" />
325
- </div>
326
- <div className="result-actions">
327
  <button
328
- id="download-btn"
329
- className="btn-download"
330
- onClick={handleDownload}
331
  >
332
- ⬇️ Download PNG Transparan
333
  </button>
334
  <button
335
- id="try-again-btn"
336
- className="btn-reset"
337
- onClick={handleReset}
338
  >
339
- 🔄 Coba Lagi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  </button>
341
  </div>
342
  </div>
343
  </div>
344
  )}
345
- </div>
346
- </main>
347
 
348
- {/* Features */}
349
- <section className="features">
350
- <div className="container">
351
- <div className="features-grid">
352
- {[
353
- { icon: '🤖', title: 'Powered by ZeroGPU', desc: 'Berjalan di HuggingFace ZeroGPU — gratis, cepat, dan tanpa antrian.' },
354
- { icon: '🎯', title: '4 Model AI', desc: 'Pilih model terbaik sesuai kebutuhan: portrait, produk, atau objek umum.' },
355
- { icon: '💎', title: 'Kualitas HD', desc: 'Hasilkan PNG transparan berkualitas tinggi tanpa kompresi.' },
356
- { icon: '🔒', title: 'Privasi Terjaga', desc: 'Gambar tidak disimpan. Diproses langsung dan langsung dibuang.' },
357
- ].map((f) => (
358
- <div key={f.title} className="feature-card">
359
- <span className="feature-icon">{f.icon}</span>
360
- <div className="feature-title">{f.title}</div>
361
- <div className="feature-desc">{f.desc}</div>
362
- </div>
363
- ))}
364
- </div>
 
 
 
 
 
 
 
 
 
365
  </div>
366
- </section>
367
 
368
  {/* Footer */}
369
  <footer className="footer">
370
  <div className="container">
371
- <p className="footer-text">
372
- © 2026 RemBG · Backend:{' '}
373
- <a href="https://huggingface.co/spaces/ilhamdev/rembg" target="_blank" rel="noopener">
374
- HuggingFace Space ilhamdev/rembg
375
- </a>{' '}
376
- · Dibuat dengan ❤️
377
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  </div>
379
  </footer>
380
  </div>
 
1
  'use client'
2
 
3
+ import { useState, useRef, useCallback, useEffect } from 'react'
4
  import { Turnstile } from '@marsidev/react-turnstile'
5
  import { DEFAULT_TEST_SITE_KEY } from '@/lib/turnstile'
 
6
 
7
  const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || DEFAULT_TEST_SITE_KEY
8
 
9
  const MODELS = [
10
+ { value: 'birefnet-portrait', label: 'BiRefNet Portrait (Optimal untuk manusia & rambut)' },
11
+ { value: 'birefnet-general', label: 'BiRefNet General (Optimal untuk produk & objek)' },
12
+ { value: 'isnet-general-use', label: 'ISNet General (Serbaguna, cepat & seimbang)' },
13
+ { value: 'isnet-anime', label: 'ISNet Anime (Optimal untuk kartun, anime & ilustrasi)' },
14
+ { value: 'u2net', label: 'U2Net (Model umum klasik yang stabil)' },
15
+ { value: 'u2net_human_seg', label: 'U2Net Human (Segmentasi manusia ringan)' },
16
  ]
17
 
18
+ function formatFileSize(bytes) {
19
+ if (!bytes) return ''
20
+ const mb = bytes / (1024 * 1024)
21
+ if (mb >= 1) return `${mb.toFixed(1)} MB`
22
+ const kb = bytes / 1024
23
+ return `${kb.toFixed(0)} KB`
24
+ }
25
+
26
  export default function Home() {
27
  const [previewUrl, setPreviewUrl] = useState(null)
28
  const [resultUrl, setResultUrl] = useState(null)
 
34
  const [progress, setProgress] = useState(0)
35
  const [dragover, setDragover] = useState(false)
36
  const [turnstileToken, setTurnstileToken] = useState(null)
37
+ const [inspectorBg, setInspectorBg] = useState('checkerboard')
38
 
39
  const fileInputRef = useRef(null)
40
  const turnstileRef = useRef(null)
41
 
42
  const handleFile = useCallback((file) => {
43
  if (!file || !file.type.startsWith('image/')) {
44
+ setError('File harus berupa gambar (JPG, PNG, atau WEBP)')
45
  return
46
  }
47
  if (file.size > 20 * 1024 * 1024) {
 
55
  setPreviewUrl(url)
56
  }, [])
57
 
58
+ // Dukungan paste gambar dari clipboard
59
+ useEffect(() => {
60
+ const handlePaste = (e) => {
61
+ const items = e.clipboardData?.items
62
+ if (!items) return
63
+ for (const item of items) {
64
+ if (item.type.startsWith('image/')) {
65
+ const file = item.getAsFile()
66
+ if (file) handleFile(file)
67
+ break
68
+ }
69
+ }
70
+ }
71
+ window.addEventListener('paste', handlePaste)
72
+ return () => window.removeEventListener('paste', handlePaste)
73
+ }, [handleFile])
74
+
75
  const handleDrop = useCallback((e) => {
76
  e.preventDefault()
77
  setDragover(false)
 
87
  const handleDragLeave = () => setDragover(false)
88
 
89
  const handleFileInput = (e) => {
90
+ if (e.target.files?.[0]) {
91
+ handleFile(e.target.files[0])
92
+ }
93
  }
94
 
95
  const handleRemoveBg = async () => {
 
105
  setResultUrl(null)
106
  setProgress(0)
107
 
 
108
  const progressInterval = setInterval(() => {
109
  setProgress(prev => Math.min(prev + Math.random() * 15, 85))
110
  }, 800)
 
127
  const data = await res.json()
128
 
129
  if (!res.ok || !data.success) {
130
+ throw new Error(data.error || 'Terjadi kesalahan pada server saat memproses gambar')
131
  }
132
 
133
+ if (!data.image) {
134
+ throw new Error('Tidak ada gambar hasil dari server')
135
+ }
136
 
137
  setProgress(100)
138
+ setResultUrl(data.image)
139
  } catch (err) {
140
  clearInterval(progressInterval)
141
  setError(err.message)
 
162
  if (!resultUrl) return
163
  const a = document.createElement('a')
164
  a.href = resultUrl
165
+ const baseName = imageFile?.name?.replace(/\.[^.]+$/, '') || 'hasil'
166
+ a.download = `${baseName}-nobg.png`
167
  a.click()
168
  }
169
 
 
173
  <header className="header">
174
  <div className="container">
175
  <div className="header-inner">
176
+ <a className="logo" href="/" aria-label="RemBG Beranda">
177
+ <span className="logo-badge" aria-hidden="true">
178
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
179
+ <circle cx="6" cy="6" r="3"/>
180
+ <circle cx="6" cy="18" r="3"/>
181
+ <line x1="20" y1="4" x2="8.12" y2="15.88"/>
182
+ <line x1="14.47" y1="14.48" x2="20" y2="20"/>
183
+ <line x1="8.12" y1="8.12" x2="12" y2="12"/>
184
+ </svg>
185
+ </span>
186
  <span className="logo-text">RemBG</span>
187
  </a>
188
+ <div className="header-meta">
189
+ <span className="system-status">
190
+ <span className="status-dot" aria-hidden="true" />
191
+ ZeroGPU Online
192
+ </span>
193
+ <a
194
+ href="https://huggingface.co/spaces/ilhamdev/rembg"
195
+ target="_blank"
196
+ rel="noreferrer"
197
+ className="hf-link"
198
+ >
199
+ Hugging Face Space
200
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
201
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
202
+ <polyline points="15 3 21 3 21 9"/>
203
+ <line x1="10" y1="14" x2="21" y2="3"/>
204
+ </svg>
205
+ </a>
206
  </div>
207
  </div>
208
  </div>
 
211
  {/* Hero */}
212
  <section className="hero">
213
  <div className="container">
214
+ <h1>Hapus Background Foto Otomatis</h1>
 
 
 
 
 
 
215
  <p className="hero-desc">
216
+ Pemisahan objek foto secara presisi dengan model BiRefNet dan ISNet. Diproses tanpa watermark dengan resolusi penuh.
217
  </p>
218
  </div>
219
  </section>
220
 
221
+ {/* Main Card */}
222
  <main>
223
  <div className="container">
224
  <div className="main-card">
225
+ {/* Konfigurasi Model */}
226
+ <div className="config-grid">
227
+ <div className="field-group">
228
+ <label htmlFor="model-select" className="field-label">Model Segmentasi</label>
229
+ <select
230
+ id="model-select"
231
+ className="select-control"
232
+ value={model}
233
+ onChange={(e) => setModel(e.target.value)}
234
+ disabled={loading}
235
+ >
236
+ {MODELS.map((m) => (
237
+ <option key={m.value} value={m.value}>{m.label}</option>
238
+ ))}
239
+ </select>
 
 
240
  </div>
241
+
242
+ <div className="field-group">
243
+ <span className="field-label">Detail Tepi</span>
244
+ <label className="toggle-card" htmlFor="alpha-toggle">
245
+ <input
246
+ id="alpha-toggle"
247
+ type="checkbox"
248
+ checked={alphaMatting}
249
+ onChange={(e) => setAlphaMatting(e.target.checked)}
250
+ disabled={loading}
251
+ />
252
+ <span className="toggle-text">
253
+ Alpha Matting (helai rambut)
 
 
 
254
  </span>
255
+ </label>
256
  </div>
257
  </div>
258
 
259
+ {/* Input Tersembunyi */}
260
  <input
261
  ref={fileInputRef}
262
  id="file-input"
263
  type="file"
264
+ accept="image/png,image/jpeg,image/webp"
265
  onChange={handleFileInput}
266
+ style={{ display: 'none' }}
267
  />
268
+
269
+ {/* Area Dropzone / Pratinjau */}
270
+ {!previewUrl ? (
271
+ <div
272
+ className={`dropzone ${dragover ? 'drag-active' : ''}`}
273
+ onClick={() => fileInputRef.current?.click()}
274
+ onDrop={handleDrop}
275
+ onDragOver={handleDragOver}
276
+ onDragLeave={handleDragLeave}
277
+ role="button"
278
+ tabIndex={0}
279
+ onKeyDown={(e) => {
280
+ if (e.key === 'Enter' || e.key === ' ') {
281
+ e.preventDefault()
282
+ fileInputRef.current?.click()
283
+ }
284
+ }}
285
+ aria-label="Area unggah gambar"
286
+ >
287
+ <div className="dropzone-inner">
288
+ <div className="dropzone-icon" aria-hidden="true">
289
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
290
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
291
+ <circle cx="8.5" cy="8.5" r="1.5"/>
292
+ <polyline points="21 15 16 10 5 21"/>
293
+ </svg>
294
  </div>
295
+ <div className="dropzone-title">Pilih file atau tarik gambar ke sini</div>
296
+ <div className="dropzone-hint">
297
+ Mendukung JPG, PNG, WEBP (maksimal 20MB) atau tempel (Ctrl+V)
298
+ </div>
299
+ </div>
300
+ </div>
301
+ ) : (
302
+ <div className="preview-container">
303
+ <div className="preview-media-box">
304
+ <img
305
+ src={previewUrl}
306
+ alt="Pratinjau foto sebelum pemisahan latar belakang"
307
+ className="preview-img"
308
+ />
309
+ </div>
310
+ <div className="preview-meta-row">
311
+ <div className="meta-info">
312
+ <span>{imageFile?.name}</span>
313
+ <span>•</span>
314
+ <span>{formatFileSize(imageFile?.size)}</span>
315
+ </div>
316
+ <button
317
+ type="button"
318
+ className="btn-ghost-small"
319
+ onClick={() => fileInputRef.current?.click()}
320
+ disabled={loading}
321
+ >
322
+ Ganti Foto
323
+ </button>
324
+ </div>
325
+ </div>
326
+ )}
327
 
328
+ {/* Indikator Progres */}
329
  {loading && (
330
+ <div className="progress-track" role="progressbar" aria-valuenow={progress} aria-valuemin={0} aria-valuemax={100}>
331
+ <div className="progress-bar-fill" style={{ width: `${progress}%` }} />
332
  </div>
333
  )}
334
 
335
+ {/* Pesan Kesalahan */}
336
  {error && (
337
+ <div className="error-banner" role="alert">
338
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
339
+ <circle cx="12" cy="12" r="10"/>
340
+ <line x1="12" y1="8" x2="12" y2="12"/>
341
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
342
+ </svg>
343
+ <span>{error}</span>
344
  </div>
345
  )}
346
 
347
  {/* Cloudflare Turnstile */}
348
+ <div className="turnstile-box">
349
  <Turnstile
350
  ref={turnstileRef}
351
  siteKey={TURNSTILE_SITE_KEY}
 
363
  size: 'normal',
364
  }}
365
  />
366
+ <span className="turnstile-notice">
367
+ Verifikasi keamanan bot Cloudflare
368
+ </span>
369
  </div>
370
 
371
+ {/* Tombol Aksi */}
372
+ <div className="action-row">
373
  <button
374
  id="remove-bg-btn"
375
+ className="btn-primary"
376
  onClick={handleRemoveBg}
377
  disabled={!imageFile || loading || !turnstileToken}
378
  >
379
  {loading ? (
380
  <>
381
+ <span className="spinner-icon" aria-hidden="true" />
382
+ Sedang Memproses...
383
  </>
384
  ) : (
385
  <>
386
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
387
+ <polyline points="20 6 9 17 4 12"/>
388
+ </svg>
389
+ Hapus Background
390
  </>
391
  )}
392
  </button>
393
  {previewUrl && (
394
  <button
395
  id="reset-btn"
396
+ className="btn-secondary"
397
  onClick={handleReset}
398
+ disabled={loading}
399
  >
400
+ Reset
401
  </button>
402
  )}
403
  </div>
404
  </div>
405
 
406
+ {/* Bagian Hasil & Inspeksi Latar Belakang */}
407
  {resultUrl && (
408
  <div className="result-section" id="result-section">
409
+ <div className="result-header">
410
+ <span className="result-title">Hasil Pemotongan Background</span>
411
+ <div className="bg-inspector-group" role="group" aria-label="Pilihan latar inspeksi">
 
 
 
 
 
412
  <button
413
+ type="button"
414
+ className={`bg-inspector-btn ${inspectorBg === 'checkerboard' ? 'active' : ''}`}
415
+ onClick={() => setInspectorBg('checkerboard')}
416
  >
417
+ Transparan
418
  </button>
419
  <button
420
+ type="button"
421
+ className={`bg-inspector-btn ${inspectorBg === 'white' ? 'active' : ''}`}
422
+ onClick={() => setInspectorBg('white')}
423
  >
424
+ Latar Putih
425
+ </button>
426
+ <button
427
+ type="button"
428
+ className={`bg-inspector-btn ${inspectorBg === 'dark' ? 'active' : ''}`}
429
+ onClick={() => setInspectorBg('dark')}
430
+ >
431
+ Latar Gelap
432
+ </button>
433
+ </div>
434
+ </div>
435
+
436
+ <div className={`result-canvas ${inspectorBg}`}>
437
+ <img
438
+ src={resultUrl}
439
+ alt="Hasil foto dengan background transparan"
440
+ className="result-image"
441
+ id="result-image"
442
+ />
443
+ </div>
444
+
445
+ <div className="result-footer-actions">
446
+ <button
447
+ id="download-btn"
448
+ className="btn-primary"
449
+ onClick={handleDownload}
450
+ >
451
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
452
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
453
+ <polyline points="7 10 12 15 17 10"/>
454
+ <line x1="12" y1="15" x2="12" y2="3"/>
455
+ </svg>
456
+ Unduh PNG Transparan
457
+ </button>
458
+ <button
459
+ id="try-again-btn"
460
+ className="btn-secondary"
461
+ onClick={handleReset}
462
+ >
463
+ Coba Foto Lain
464
+ </button>
465
+ </div>
466
+
467
+ {/* Panel Proses Ulang dengan Model Alternatif */}
468
+ <div className="reprocess-panel">
469
+ <div className="reprocess-header">
470
+ <span className="reprocess-title">Hasil Kurang Pas? Coba Model Alternatif</span>
471
+ <span className="reprocess-desc">
472
+ Ubah model atau aktifkan Alpha Matting lalu klik proses ulang langsung pada foto ini tanpa upload ulang.
473
+ </span>
474
+ </div>
475
+ <div className="reprocess-controls">
476
+ <div className="reprocess-field">
477
+ <select
478
+ id="reprocess-model-select"
479
+ className="select-control"
480
+ value={model}
481
+ onChange={(e) => setModel(e.target.value)}
482
+ disabled={loading}
483
+ >
484
+ {MODELS.map((m) => (
485
+ <option key={m.value} value={m.value}>{m.label}</option>
486
+ ))}
487
+ </select>
488
+ </div>
489
+ <label className="toggle-card" htmlFor="reprocess-alpha-toggle">
490
+ <input
491
+ id="reprocess-alpha-toggle"
492
+ type="checkbox"
493
+ checked={alphaMatting}
494
+ onChange={(e) => setAlphaMatting(e.target.checked)}
495
+ disabled={loading}
496
+ />
497
+ <span className="toggle-text">
498
+ Alpha Matting
499
+ </span>
500
+ </label>
501
+ <button
502
+ type="button"
503
+ className="btn-secondary"
504
+ onClick={handleRemoveBg}
505
+ disabled={loading || !turnstileToken}
506
+ >
507
+ {loading ? (
508
+ <>
509
+ <span className="spinner-icon" aria-hidden="true" />
510
+ Memproses...
511
+ </>
512
+ ) : (
513
+ <>
514
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
515
+ <polyline points="23 4 23 10 17 10"/>
516
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
517
+ </svg>
518
+ Proses Ulang
519
+ </>
520
+ )}
521
  </button>
522
  </div>
523
  </div>
524
  </div>
525
  )}
 
 
526
 
527
+ {/* Informasi Teknis & Spesifikasi Proyek */}
528
+ <section className="info-section">
529
+ <div className="info-grid">
530
+ <article className="info-card">
531
+ <h2>Panduan Model AI</h2>
532
+ <p>Pilih model yang tepat untuk hasil segmentasi paling optimal:</p>
533
+ <ul className="model-list">
534
+ <li><strong>BiRefNet Portrait:</strong> Dirancang khusus untuk foto orang, mendeteksi lekuk pakaian dan helai rambut secara halus.</li>
535
+ <li><strong>BiRefNet General:</strong> Optimal untuk foto produk, kemasan e-commerce, dan benda padat.</li>
536
+ <li><strong>ISNet General:</strong> Model seimbang dengan kecepatan inferensi tinggi dan kualitas tajam.</li>
537
+ <li><strong>ISNet Anime:</strong> Spesialis gambar kartun, ilustrasi 2D, anime, dan grafik vektor.</li>
538
+ <li><strong>U2Net:</strong> Model umum klasik dengan toleransi tinggi terhadap berbagai tipe objek.</li>
539
+ <li><strong>U2Net Human:</strong> Segmentasi manusia ringan untuk pemrosesan cepat.</li>
540
+ </ul>
541
+ </article>
542
+
543
+ <article className="info-card">
544
+ <h2>Spesifikasi & Privasi</h2>
545
+ <ul className="model-list">
546
+ <li><strong>Format Masukan:</strong> Mendukung JPG, JPEG, PNG, dan WEBP hingga ukuran 20MB.</li>
547
+ <li><strong>Format Keluaran:</strong> Format PNG 32-bit dengan saluran alpha transparan penuh tanpa kompresi rusak.</li>
548
+ <li><strong>Pemrosesan Aman:</strong> Gambar diproses langsung pada memori inferensi Hugging Face ZeroGPU dan tidak disimpan secara permanen pada server.</li>
549
+ </ul>
550
+ </article>
551
+ </div>
552
+ </section>
553
  </div>
554
+ </main>
555
 
556
  {/* Footer */}
557
  <footer className="footer">
558
  <div className="container">
559
+ <div className="footer-content">
560
+ <span className="footer-text">
561
+ RemBG: Penghapus Background Foto AI
562
+ </span>
563
+ <div className="footer-links">
564
+ <a
565
+ href="https://huggingface.co/spaces/ilhamdev/rembg"
566
+ target="_blank"
567
+ rel="noreferrer"
568
+ className="footer-link"
569
+ >
570
+ Hugging Face ZeroGPU Space
571
+ </a>
572
+ <a
573
+ href="https://github.com/danielgatis/rembg"
574
+ target="_blank"
575
+ rel="noreferrer"
576
+ className="footer-link"
577
+ >
578
+ Pustaka rembg Python
579
+ </a>
580
+ </div>
581
+ </div>
582
  </div>
583
  </footer>
584
  </div>
frontend/src/app/robots.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || 'https://rembg.vercel.app').replace(/\/+$/, '')
2
+
3
+ export default function robots() {
4
+ return {
5
+ rules: {
6
+ userAgent: '*',
7
+ allow: '/',
8
+ disallow: ['/api/'],
9
+ },
10
+ sitemap: `${SITE_URL}/sitemap.xml`,
11
+ }
12
+ }
frontend/src/app/sitemap.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || 'https://rembg.vercel.app').replace(/\/+$/, '')
2
+
3
+ export default function sitemap() {
4
+ return [
5
+ {
6
+ url: SITE_URL,
7
+ lastModified: new Date(),
8
+ changeFrequency: 'weekly',
9
+ priority: 1.0,
10
+ },
11
+ ]
12
+ }