Spaces:
Runtime error
Runtime error
Commit ·
a3a9068
1
Parent(s): 6b7bc46
feat: Add dark mode toggle and update UI components with theme-aware styling.
Browse files- dev.log +311 -0
- src/app/dashboard/page.tsx +11 -13
- src/app/globals.css +15 -2
- src/components/auth/sign-in-form.tsx +1 -1
- src/components/auth/sign-up-form.tsx +1 -1
- src/components/auth/user-nav.tsx +48 -43
- src/components/mode-toggle.tsx +40 -0
- src/features/analytics/components/key-metrics-row.tsx +14 -14
- src/features/dashboard/components/video-list-row.tsx +6 -6
- src/features/dashboard/components/video-workbench.tsx +19 -19
- src/features/discovery/components/youtube-connect-section.tsx +9 -9
- src/features/vault/components/affiliate-submission-dialog.tsx +7 -7
- src/features/vault/components/product-request-dialog.tsx +8 -8
- src/features/vault/components/quick-analyze-tool.tsx +26 -27
dev.log
CHANGED
|
@@ -516,3 +516,314 @@ Re-triggering analysis for 18BsJxwYETw (status: pending)
|
|
| 516 |
POST /dashboard 200 in 2440ms
|
| 517 |
GET /api/auth/get-session 200 in 677ms
|
| 518 |
POST /dashboard 200 in 765ms
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
POST /dashboard 200 in 2440ms
|
| 517 |
GET /api/auth/get-session 200 in 677ms
|
| 518 |
POST /dashboard 200 in 765ms
|
| 519 |
+
✓ Compiled in 220ms
|
| 520 |
+
✓ Compiled in 395ms
|
| 521 |
+
✓ Compiled in 100ms
|
| 522 |
+
✓ Compiled in 346ms
|
| 523 |
+
GET /api/auth/get-session 200 in 1467ms
|
| 524 |
+
✓ Compiled /dashboard in 29ms
|
| 525 |
+
GET /api/auth/get-session 200 in 662ms
|
| 526 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 527 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 528 |
+
GET /dashboard 200 in 3416ms
|
| 529 |
+
GET /dashboard 200 in 2649ms
|
| 530 |
+
✓ Compiled / in 59ms
|
| 531 |
+
Cache miss - fetching trending content from database
|
| 532 |
+
Cache miss - fetching featured creators from database
|
| 533 |
+
GET / 200 in 2106ms
|
| 534 |
+
✓ Compiled /_not-found/page in 298ms
|
| 535 |
+
GET /placeholder-product.png 404 in 357ms
|
| 536 |
+
⨯ The requested resource isn't a valid image for /placeholder-product.png received null
|
| 537 |
+
✓ Compiled in 111ms
|
| 538 |
+
Cache miss - fetching trending content from database
|
| 539 |
+
Cache miss - fetching featured creators from database
|
| 540 |
+
✓ Compiled in 239ms
|
| 541 |
+
GET / 200 in 388ms
|
| 542 |
+
GET /api/auth/get-session 200 in 1568ms
|
| 543 |
+
✓ Compiled /dashboard in 41ms
|
| 544 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 545 |
+
✓ Compiled in 91ms
|
| 546 |
+
GET /dashboard 200 in 3161ms
|
| 547 |
+
✓ Compiled in 492ms
|
| 548 |
+
✓ Compiled in 108ms
|
| 549 |
+
✓ Compiled in 123ms
|
| 550 |
+
✓ Compiled in 135ms
|
| 551 |
+
✓ Compiled in 93ms
|
| 552 |
+
✓ Compiled in 161ms
|
| 553 |
+
✓ Compiled in 87ms
|
| 554 |
+
Cache miss - fetching trending content from database
|
| 555 |
+
Cache miss - fetching featured creators from database
|
| 556 |
+
GET / 200 in 1940ms
|
| 557 |
+
GET /favicon.ico?favicon.0b3bf435.ico 200 in 380ms
|
| 558 |
+
GET /api/auth/get-session 200 in 1456ms
|
| 559 |
+
GET /api/auth/get-session 200 in 1503ms
|
| 560 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 561 |
+
GET /dashboard 200 in 4082ms
|
| 562 |
+
GET /api/auth/get-session 200 in 562ms
|
| 563 |
+
GET /api/auth/get-session 200 in 557ms
|
| 564 |
+
POST /dashboard 200 in 1638ms
|
| 565 |
+
GET /api/auth/get-session 200 in 1418ms
|
| 566 |
+
○ Compiling /dashboard/moderation ...
|
| 567 |
+
✓ Compiled /dashboard/moderation in 593ms
|
| 568 |
+
GET /dashboard/moderation 200 in 2911ms
|
| 569 |
+
GET /api/auth/get-session 200 in 620ms
|
| 570 |
+
POST /dashboard 200 in 1723ms
|
| 571 |
+
GET /api/auth/get-session 200 in 627ms
|
| 572 |
+
POST /dashboard 200 in 765ms
|
| 573 |
+
GET /api/auth/get-session 200 in 1451ms
|
| 574 |
+
✓ Compiled in 465ms
|
| 575 |
+
✓ Compiled / in 117ms
|
| 576 |
+
Cache miss - fetching trending content from database
|
| 577 |
+
Cache miss - fetching featured creators from database
|
| 578 |
+
Cache miss - fetching trending content from database
|
| 579 |
+
Cache miss - fetching featured creators from database
|
| 580 |
+
GET / 200 in 1111ms
|
| 581 |
+
GET / 200 in 1685ms
|
| 582 |
+
Cache miss - fetching trending content from database
|
| 583 |
+
Cache miss - fetching featured creators from database
|
| 584 |
+
✓ Compiled in 130ms
|
| 585 |
+
GET /api/auth/get-session 200 in 1354ms
|
| 586 |
+
GET / 200 in 223ms
|
| 587 |
+
✓ Compiled /dashboard in 39ms
|
| 588 |
+
GET /api/auth/get-session 200 in 741ms
|
| 589 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 590 |
+
GET /api/auth/get-session 200 in 557ms
|
| 591 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 592 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 593 |
+
GET /dashboard 200 in 2891ms
|
| 594 |
+
GET /dashboard 200 in 2113ms
|
| 595 |
+
GET /dashboard 200 in 2737ms
|
| 596 |
+
✓ Compiled /showcase/[videoId] in 48ms
|
| 597 |
+
Self-healing: Re-triggering stuck analysis for video iumWFCvrLLY
|
| 598 |
+
[ingestAndAnalyze] START - videoId: iumWFCvrLLY, userId: undefined, skipAnalysis: undefined
|
| 599 |
+
[ingestAndAnalyze] Fetching oembed for iumWFCvrLLY...
|
| 600 |
+
GET /showcase/iumWFCvrLLY 200 in 1568ms
|
| 601 |
+
[ingestAndAnalyze] oembed fetch failed with status 403. Trying API fallback...
|
| 602 |
+
[ingestAndAnalyze] Trying full API fallback for iumWFCvrLLY...
|
| 603 |
+
YouTube API fallback failed: Error: Video not found in YouTube API
|
| 604 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:411:35)
|
| 605 |
+
409 |
|
| 606 |
+
410 | if (!videoApiData.items?.[0]) {
|
| 607 |
+
> 411 | throw new Error('Video not found in YouTube API');
|
| 608 |
+
| ^
|
| 609 |
+
412 | }
|
| 610 |
+
413 |
|
| 611 |
+
414 | const snippet = videoApiData.items[0].snippet;
|
| 612 |
+
Showcase ingest error (detailed): {
|
| 613 |
+
error: Error: Failed to fetch video metadata via API
|
| 614 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:434:31)
|
| 615 |
+
432 | } catch (apiError) {
|
| 616 |
+
433 | console.error('YouTube API fallback failed:', apiError);
|
| 617 |
+
> 434 | throw new Error('Failed to fetch video metadata via API');
|
| 618 |
+
| ^
|
| 619 |
+
435 | }
|
| 620 |
+
436 | } else {
|
| 621 |
+
437 | throw new Error('Video unavailable (OEmbed failed and no API key configured)');,
|
| 622 |
+
videoId: 'iumWFCvrLLY',
|
| 623 |
+
stack: 'Error: Failed to fetch video metadata via API\n' +
|
| 624 |
+
' at ShowcaseService.ingestAndAnalyze (/Users/vijaykrishnan/Dev/Vault/.next/server/chunks/ssr/[root-of-the-server]__85a520ff._.js:2202:31)\n' +
|
| 625 |
+
' at process.processTicksAndRejections (node:internal/process/task_queues:105:5)'
|
| 626 |
+
}
|
| 627 |
+
GET /favicon.ico?favicon.0b3bf435.ico 200 in 270ms
|
| 628 |
+
GET /api/auth/get-session 200 in 565ms
|
| 629 |
+
POST /dashboard 200 in 805ms
|
| 630 |
+
GET /api/auth/get-session 200 in 664ms
|
| 631 |
+
POST /dashboard 200 in 809ms
|
| 632 |
+
✓ Compiled in 145ms
|
| 633 |
+
✓ Compiled in 127ms
|
| 634 |
+
Self-healing: Re-triggering stuck analysis for video UzVH2TzvwiU
|
| 635 |
+
[ingestAndAnalyze] START - videoId: UzVH2TzvwiU, userId: undefined, skipAnalysis: undefined
|
| 636 |
+
[ingestAndAnalyze] Fetching oembed for UzVH2TzvwiU...
|
| 637 |
+
GET /showcase/UzVH2TzvwiU 200 in 1180ms
|
| 638 |
+
[ingestAndAnalyze] oembed fetch failed with status 403. Trying API fallback...
|
| 639 |
+
[ingestAndAnalyze] Trying full API fallback for UzVH2TzvwiU...
|
| 640 |
+
YouTube API fallback failed: Error: Video not found in YouTube API
|
| 641 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:411:35)
|
| 642 |
+
409 |
|
| 643 |
+
410 | if (!videoApiData.items?.[0]) {
|
| 644 |
+
> 411 | throw new Error('Video not found in YouTube API');
|
| 645 |
+
| ^
|
| 646 |
+
412 | }
|
| 647 |
+
413 |
|
| 648 |
+
414 | const snippet = videoApiData.items[0].snippet;
|
| 649 |
+
Showcase ingest error (detailed): {
|
| 650 |
+
error: Error: Failed to fetch video metadata via API
|
| 651 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:434:31)
|
| 652 |
+
432 | } catch (apiError) {
|
| 653 |
+
433 | console.error('YouTube API fallback failed:', apiError);
|
| 654 |
+
> 434 | throw new Error('Failed to fetch video metadata via API');
|
| 655 |
+
| ^
|
| 656 |
+
435 | }
|
| 657 |
+
436 | } else {
|
| 658 |
+
437 | throw new Error('Video unavailable (OEmbed failed and no API key configured)');,
|
| 659 |
+
videoId: 'UzVH2TzvwiU',
|
| 660 |
+
stack: 'Error: Failed to fetch video metadata via API\n' +
|
| 661 |
+
' at ShowcaseService.ingestAndAnalyze (/Users/vijaykrishnan/Dev/Vault/.next/server/chunks/ssr/[root-of-the-server]__85a520ff._.js:2202:31)\n' +
|
| 662 |
+
' at process.processTicksAndRejections (node:internal/process/task_queues:105:5)'
|
| 663 |
+
}
|
| 664 |
+
✓ Compiled in 118ms
|
| 665 |
+
GET /api/auth/get-session 200 in 1476ms
|
| 666 |
+
POST /dashboard 200 in 2987ms
|
| 667 |
+
GET /api/auth/get-session 200 in 648ms
|
| 668 |
+
POST /dashboard 200 in 1012ms
|
| 669 |
+
Self-healing: Re-triggering stuck analysis for video fqXkCp2zj30
|
| 670 |
+
[ingestAndAnalyze] START - videoId: fqXkCp2zj30, userId: undefined, skipAnalysis: undefined
|
| 671 |
+
[ingestAndAnalyze] Fetching oembed for fqXkCp2zj30...
|
| 672 |
+
GET /showcase/fqXkCp2zj30 200 in 1424ms
|
| 673 |
+
[ingestAndAnalyze] oembed fetch failed with status 403. Trying API fallback...
|
| 674 |
+
[ingestAndAnalyze] Trying full API fallback for fqXkCp2zj30...
|
| 675 |
+
YouTube API fallback failed: Error: Video not found in YouTube API
|
| 676 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:411:35)
|
| 677 |
+
409 |
|
| 678 |
+
410 | if (!videoApiData.items?.[0]) {
|
| 679 |
+
> 411 | throw new Error('Video not found in YouTube API');
|
| 680 |
+
| ^
|
| 681 |
+
412 | }
|
| 682 |
+
413 |
|
| 683 |
+
414 | const snippet = videoApiData.items[0].snippet;
|
| 684 |
+
Showcase ingest error (detailed): {
|
| 685 |
+
error: Error: Failed to fetch video metadata via API
|
| 686 |
+
at ShowcaseService.ingestAndAnalyze (src/features/vault/services/showcase.service.ts:434:31)
|
| 687 |
+
432 | } catch (apiError) {
|
| 688 |
+
433 | console.error('YouTube API fallback failed:', apiError);
|
| 689 |
+
> 434 | throw new Error('Failed to fetch video metadata via API');
|
| 690 |
+
| ^
|
| 691 |
+
435 | }
|
| 692 |
+
436 | } else {
|
| 693 |
+
437 | throw new Error('Video unavailable (OEmbed failed and no API key configured)');,
|
| 694 |
+
videoId: 'fqXkCp2zj30',
|
| 695 |
+
stack: 'Error: Failed to fetch video metadata via API\n' +
|
| 696 |
+
' at ShowcaseService.ingestAndAnalyze (/Users/vijaykrishnan/Dev/Vault/.next/server/chunks/ssr/[root-of-the-server]__85a520ff._.js:2202:31)\n' +
|
| 697 |
+
' at process.processTicksAndRejections (node:internal/process/task_queues:105:5)'
|
| 698 |
+
}
|
| 699 |
+
GET /api/auth/get-session 200 in 824ms
|
| 700 |
+
POST /dashboard 200 in 811ms
|
| 701 |
+
GET /api/auth/get-session 200 in 692ms
|
| 702 |
+
POST /dashboard 200 in 762ms
|
| 703 |
+
✓ Compiled in 232ms
|
| 704 |
+
Cache miss - fetching trending content from database
|
| 705 |
+
Cache miss - fetching featured creators from database
|
| 706 |
+
GET / 200 in 1636ms
|
| 707 |
+
GET /api/auth/get-session 200 in 1868ms
|
| 708 |
+
✓ Compiled /dashboard in 100ms
|
| 709 |
+
[DashboardPage] userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 710 |
+
GET /dashboard 200 in 3179ms
|
| 711 |
+
GET /api/auth/get-session 200 in 1457ms
|
| 712 |
+
GET /api/auth/get-session 200 in 563ms
|
| 713 |
+
[quickAddVideoAction] Video ID parsed: mWkXURsMAMk. Calling ingestAndAnalyze with skipAnalysis: true...
|
| 714 |
+
[ingestAndAnalyze] START - videoId: mWkXURsMAMk, userId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw, skipAnalysis: true
|
| 715 |
+
[ingestAndAnalyze] Fetching oembed for mWkXURsMAMk...
|
| 716 |
+
[ingestAndAnalyze] OEmbed Metadata fetched: ✅ Best eReader 2026 [Watch This Before You Make a Choice!]
|
| 717 |
+
[ingestAndAnalyze] Generated slug: theforemostpicks2026 for author: Foremost Picks
|
| 718 |
+
[ingestAndAnalyze] Slug 'theforemostpicks2026' already taken. Generating unique variation.
|
| 719 |
+
[ingestAndAnalyze] Creating NEW channel for creatorId: pvcRv8Ruf0FqiKwPLSUYjpMhBkTnyhJw
|
| 720 |
+
[ingestAndAnalyze] Attempting to prepopulate products from description for mWkXURsMAMk...
|
| 721 |
+
[prepopulate] Found 6 potential links to process.
|
| 722 |
+
[prepopulate] Processing link: https://geni.us/Y2An4
|
| 723 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 724 |
+
REDIRECT_RESOLVE: Checking https://geni.us/Y2An4
|
| 725 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/Y2An4 -> https://www.amazon.com/dp/B0DSZWF122?tag=top5pickksof-20&geniuslink=true
|
| 726 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 727 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 728 |
+
Could not parse CSS stylesheet
|
| 729 |
+
Could not parse CSS stylesheet
|
| 730 |
+
Could not parse CSS stylesheet
|
| 731 |
+
Could not parse CSS stylesheet
|
| 732 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 733 |
+
hasImage: true,
|
| 734 |
+
imageUrl: 'https://m.media-amazon.com/images/I/51lam7v+AwL._AC_SL1000_.jpg',
|
| 735 |
+
hasName: true
|
| 736 |
+
}
|
| 737 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0DSZWF122?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0DSZWF122?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (2766645 bytes)]
|
| 738 |
+
[prepopulate] Successfully added product: BOOX Palma2 Mobile ePaper eBook Reader Fingerprint Recognition Smart Button Dual Microphones Off White
|
| 739 |
+
[prepopulate] Processing link: https://geni.us/vBxa228
|
| 740 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 741 |
+
REDIRECT_RESOLVE: Checking https://geni.us/vBxa228
|
| 742 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/vBxa228 -> https://www.amazon.com/dp/B0CFPJYX7P?tag=top5pickksof-20&geniuslink=true
|
| 743 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 744 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 745 |
+
Could not parse CSS stylesheet
|
| 746 |
+
Could not parse CSS stylesheet
|
| 747 |
+
Could not parse CSS stylesheet
|
| 748 |
+
Could not parse CSS stylesheet
|
| 749 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 750 |
+
hasImage: true,
|
| 751 |
+
imageUrl: 'https://m.media-amazon.com/images/I/71YwNBmu+aL._AC_SL1500_.jpg',
|
| 752 |
+
hasName: true
|
| 753 |
+
}
|
| 754 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0CFPJYX7P?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0CFPJYX7P?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (2648509 bytes)]
|
| 755 |
+
[prepopulate] Successfully added product: Amazon Kindle Paperwhite 16GB (newest model) – 20% faster, with new 7" glare-free display and weeks of battery life – Black
|
| 756 |
+
[prepopulate] Processing link: https://geni.us/ZNv6u
|
| 757 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 758 |
+
REDIRECT_RESOLVE: Checking https://geni.us/ZNv6u
|
| 759 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/ZNv6u -> https://www.amazon.com/dp/B0CZXWGK79?tag=top5pickksof-20&geniuslink=true
|
| 760 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 761 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 762 |
+
Could not parse CSS stylesheet
|
| 763 |
+
Could not parse CSS stylesheet
|
| 764 |
+
Could not parse CSS stylesheet
|
| 765 |
+
Could not parse CSS stylesheet
|
| 766 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 767 |
+
hasImage: true,
|
| 768 |
+
imageUrl: 'https://m.media-amazon.com/images/I/61Y4ELO5kOL._AC_SL1080_.jpg',
|
| 769 |
+
hasName: true
|
| 770 |
+
}
|
| 771 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0CZXWGK79?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0CZXWGK79?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (3351549 bytes)]
|
| 772 |
+
[prepopulate] Successfully added product: Kobo Libra Colour
|
| 773 |
+
[prepopulate] Processing link: https://geni.us/2xbHMQ
|
| 774 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 775 |
+
REDIRECT_RESOLVE: Checking https://geni.us/2xbHMQ
|
| 776 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/2xbHMQ -> https://www.amazon.com/dp/B0CGVSKR1G?tag=top5pickksof-20&geniuslink=true
|
| 777 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 778 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 779 |
+
Could not parse CSS stylesheet
|
| 780 |
+
Could not parse CSS stylesheet
|
| 781 |
+
Could not parse CSS stylesheet
|
| 782 |
+
Could not parse CSS stylesheet
|
| 783 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 784 |
+
hasImage: true,
|
| 785 |
+
imageUrl: 'https://m.media-amazon.com/images/I/71KFrD7KfCL._AC_SL1500_.jpg',
|
| 786 |
+
hasName: true
|
| 787 |
+
}
|
| 788 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0CGVSKR1G?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0CGVSKR1G?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (2513853 bytes)]
|
| 789 |
+
[prepopulate] Successfully added product: Amazon Kindle Colorsoft 16 GB (newest model) – With color display and adjustable warm light – No Ads – Black
|
| 790 |
+
[prepopulate] Processing link: https://geni.us/TGrtk
|
| 791 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 792 |
+
REDIRECT_RESOLVE: Checking https://geni.us/TGrtk
|
| 793 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/TGrtk -> https://www.amazon.com/dp/B0CZ9T2C6B?tag=top5pickksof-20&geniuslink=true
|
| 794 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 795 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 796 |
+
Could not parse CSS stylesheet
|
| 797 |
+
Could not parse CSS stylesheet
|
| 798 |
+
Could not parse CSS stylesheet
|
| 799 |
+
Could not parse CSS stylesheet
|
| 800 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 801 |
+
hasImage: true,
|
| 802 |
+
imageUrl: 'https://m.media-amazon.com/images/I/81xUpeu-LzL._AC_SL1500_.jpg',
|
| 803 |
+
hasName: true
|
| 804 |
+
}
|
| 805 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0CZ9T2C6B?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0CZ9T2C6B?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (2630133 bytes)]
|
| 806 |
+
[prepopulate] Successfully added product: Amazon Kindle Scribe (64GB) - Your notes, documents and books, all in one place. With built-in AI notebook summarization. Includes Premium Pen - Tungsten
|
| 807 |
+
[prepopulate] Processing link: https://geni.us/seABI
|
| 808 |
+
METADATA_FETCH: Unknown domain geni.us, attempting redirect resolution...
|
| 809 |
+
REDIRECT_RESOLVE: Checking https://geni.us/seABI
|
| 810 |
+
REDIRECT_RESOLVE: Resolved https://geni.us/seABI -> https://www.amazon.com/dp/B0DG9ZXWMK?tag=top5pickksof-20&geniuslink=true
|
| 811 |
+
METADATA_FETCH: Initializing for www.amazon.com
|
| 812 |
+
METADATA_FETCH: Service credentials missing, trying FallbackScraper for amazon
|
| 813 |
+
Could not parse CSS stylesheet
|
| 814 |
+
Could not parse CSS stylesheet
|
| 815 |
+
Could not parse CSS stylesheet
|
| 816 |
+
Could not parse CSS stylesheet
|
| 817 |
+
METADATA_FETCH: FallbackScraper completed for amazon {
|
| 818 |
+
hasImage: true,
|
| 819 |
+
imageUrl: 'https://m.media-amazon.com/images/I/71dzWnEOQvL._AC_SL1500_.jpg',
|
| 820 |
+
hasName: true
|
| 821 |
+
}
|
| 822 |
+
Failed to set fetch cache https://www.amazon.com/dp/B0DG9ZXWMK?tag=top5pickksof-20&geniuslink=true [Error: Failed to set Next.js data cache for https://www.amazon.com/dp/B0DG9ZXWMK?tag=top5pickksof-20&geniuslink=true, items over 2MB can not be cached (3577397 bytes)]
|
| 823 |
+
[prepopulate] Successfully added product: reMarkable Paper Pro Bundle – Includes 11.8” reMarkable Paper Tablet, and Marker Plus Pen with Eraser
|
| 824 |
+
[quickAddVideoAction] ingestAndAnalyze result for mWkXURsMAMk: { success: true }
|
| 825 |
+
POST /dashboard 200 in 21445ms
|
| 826 |
+
GET /showcase/mWkXURsMAMk 200 in 1619ms
|
| 827 |
+
GET /showcase/mWkXURsMAMk 200 in 1565ms
|
| 828 |
+
✓ Compiled /register in 299ms
|
| 829 |
+
GET /register?videoId=mWkXURsMAMk 200 in 335ms
|
src/app/dashboard/page.tsx
CHANGED
|
@@ -174,17 +174,20 @@ export default async function DashboardPage({
|
|
| 174 |
{/* Key Metrics Row */}
|
| 175 |
<KeyMetricsRow />
|
| 176 |
|
| 177 |
-
{/*
|
|
|
|
|
|
|
|
|
|
| 178 |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 179 |
<Link href="/dashboard/moderation" className="group">
|
| 180 |
-
<Card className="glass
|
| 181 |
<CardContent className="flex items-center gap-4 p-4">
|
| 182 |
-
<div className="p-2.5 bg-
|
| 183 |
<ShieldCheck className="h-5 w-5 text-amber-400" />
|
| 184 |
</div>
|
| 185 |
<div className="flex-1 min-w-0">
|
| 186 |
<p className="text-sm font-medium">Product Review</p>
|
| 187 |
-
<p className="text-xs text-muted-foreground">Review AI
|
| 188 |
</div>
|
| 189 |
{pendingCount > 0 && (
|
| 190 |
<Badge className="bg-amber-500 text-white text-xs">{pendingCount}</Badge>
|
|
@@ -192,11 +195,12 @@ export default async function DashboardPage({
|
|
| 192 |
</CardContent>
|
| 193 |
</Card>
|
| 194 |
</Link>
|
|
|
|
| 195 |
{channels.length > 0 && channels[0].creatorSlug && (
|
| 196 |
<Link href={`/vault/${channels[0].creatorSlug}`} className="group">
|
| 197 |
-
<Card className="glass
|
| 198 |
<CardContent className="flex items-center gap-4 p-4">
|
| 199 |
-
<div className="p-2.5 bg-
|
| 200 |
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 201 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 202 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
@@ -204,7 +208,7 @@ export default async function DashboardPage({
|
|
| 204 |
</div>
|
| 205 |
<div className="flex-1 min-w-0">
|
| 206 |
<p className="text-sm font-medium">View Public Vault</p>
|
| 207 |
-
<p className="text-xs text-muted-foreground">See your
|
| 208 |
</div>
|
| 209 |
</CardContent>
|
| 210 |
</Card>
|
|
@@ -217,12 +221,6 @@ export default async function DashboardPage({
|
|
| 217 |
channels={channels}
|
| 218 |
isGoogleLinked={!!googleAccount?.accessToken}
|
| 219 |
/>
|
| 220 |
-
|
| 221 |
-
<div className="mt-8 border-t border-white/5 pt-8">
|
| 222 |
-
<div className="max-w-xl">
|
| 223 |
-
<QuickAnalyzeTool />
|
| 224 |
-
</div>
|
| 225 |
-
</div>
|
| 226 |
</div>
|
| 227 |
|
| 228 |
</div>
|
|
|
|
| 174 |
{/* Key Metrics Row */}
|
| 175 |
<KeyMetricsRow />
|
| 176 |
|
| 177 |
+
{/* Add to Vault - Full Width */}
|
| 178 |
+
<QuickAnalyzeTool />
|
| 179 |
+
|
| 180 |
+
{/* Quick Actions Row */}
|
| 181 |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 182 |
<Link href="/dashboard/moderation" className="group">
|
| 183 |
+
<Card className="glass hover:bg-white/[0.07] dark:hover:bg-white/[0.05] transition-all duration-300 cursor-pointer">
|
| 184 |
<CardContent className="flex items-center gap-4 p-4">
|
| 185 |
+
<div className="p-2.5 bg-background border border-border group-hover:scale-110 transition-transform rounded-lg">
|
| 186 |
<ShieldCheck className="h-5 w-5 text-amber-400" />
|
| 187 |
</div>
|
| 188 |
<div className="flex-1 min-w-0">
|
| 189 |
<p className="text-sm font-medium">Product Review</p>
|
| 190 |
+
<p className="text-xs text-muted-foreground">Review AI detections</p>
|
| 191 |
</div>
|
| 192 |
{pendingCount > 0 && (
|
| 193 |
<Badge className="bg-amber-500 text-white text-xs">{pendingCount}</Badge>
|
|
|
|
| 195 |
</CardContent>
|
| 196 |
</Card>
|
| 197 |
</Link>
|
| 198 |
+
|
| 199 |
{channels.length > 0 && channels[0].creatorSlug && (
|
| 200 |
<Link href={`/vault/${channels[0].creatorSlug}`} className="group">
|
| 201 |
+
<Card className="glass hover:bg-white/[0.07] dark:hover:bg-white/[0.05] transition-all duration-300 cursor-pointer">
|
| 202 |
<CardContent className="flex items-center gap-4 p-4">
|
| 203 |
+
<div className="p-2.5 bg-background border border-border group-hover:scale-110 transition-transform rounded-lg">
|
| 204 |
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 205 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 206 |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
|
|
| 208 |
</div>
|
| 209 |
<div className="flex-1 min-w-0">
|
| 210 |
<p className="text-sm font-medium">View Public Vault</p>
|
| 211 |
+
<p className="text-xs text-muted-foreground">See your storefront</p>
|
| 212 |
</div>
|
| 213 |
</CardContent>
|
| 214 |
</Card>
|
|
|
|
| 221 |
channels={channels}
|
| 222 |
isGoogleLinked={!!googleAccount?.accessToken}
|
| 223 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
</div>
|
| 225 |
|
| 226 |
</div>
|
src/app/globals.css
CHANGED
|
@@ -103,12 +103,25 @@
|
|
| 103 |
}
|
| 104 |
|
| 105 |
/* Glassmorphism utility */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
.glass {
|
| 107 |
-
@apply
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
.glass-panel {
|
| 111 |
-
@apply
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
/* Animations */
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
/* Glassmorphism utility */
|
| 106 |
+
:root {
|
| 107 |
+
--glass-bg: 0 0% 0% / 0.03;
|
| 108 |
+
--glass-border: 0 0% 0% / 0.08;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.dark {
|
| 112 |
+
--glass-bg: 0 0% 100% / 0.05;
|
| 113 |
+
--glass-border: 0 0% 100% / 0.1;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
.glass {
|
| 117 |
+
@apply backdrop-blur-md transition-all duration-300;
|
| 118 |
+
background-color: hsl(var(--glass-bg));
|
| 119 |
+
border: 1px solid hsl(var(--glass-border));
|
| 120 |
}
|
| 121 |
|
| 122 |
.glass-panel {
|
| 123 |
+
@apply backdrop-blur-xl border border-border transition-all duration-300;
|
| 124 |
+
@apply bg-background/60;
|
| 125 |
}
|
| 126 |
|
| 127 |
/* Animations */
|
src/components/auth/sign-in-form.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export function SignInForm() {
|
|
| 42 |
type="button"
|
| 43 |
disabled={loading}
|
| 44 |
onClick={handleGoogleSignIn}
|
| 45 |
-
className="w-full h-11 text-base
|
| 46 |
>
|
| 47 |
{loading ? (
|
| 48 |
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
|
|
| 42 |
type="button"
|
| 43 |
disabled={loading}
|
| 44 |
onClick={handleGoogleSignIn}
|
| 45 |
+
className="w-full h-11 text-base border-border hover:bg-secondary transition-all duration-300"
|
| 46 |
>
|
| 47 |
{loading ? (
|
| 48 |
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
src/components/auth/sign-up-form.tsx
CHANGED
|
@@ -42,7 +42,7 @@ export function SignUpForm() {
|
|
| 42 |
type="button"
|
| 43 |
disabled={loading}
|
| 44 |
onClick={handleGoogleSignIn}
|
| 45 |
-
className="w-full h-11 text-base
|
| 46 |
>
|
| 47 |
{loading ? (
|
| 48 |
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
|
|
| 42 |
type="button"
|
| 43 |
disabled={loading}
|
| 44 |
onClick={handleGoogleSignIn}
|
| 45 |
+
className="w-full h-11 text-base border-border hover:bg-secondary transition-all duration-300"
|
| 46 |
>
|
| 47 |
{loading ? (
|
| 48 |
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
src/components/auth/user-nav.tsx
CHANGED
|
@@ -15,6 +15,8 @@ import { Button } from "@/components/ui/button";
|
|
| 15 |
import { LogOut, User, LayoutDashboard, Settings } from "lucide-react";
|
| 16 |
import Link from "next/link";
|
| 17 |
|
|
|
|
|
|
|
| 18 |
export function UserNav() {
|
| 19 |
const { data: session } = useSession();
|
| 20 |
const router = useRouter();
|
|
@@ -39,48 +41,51 @@ export function UserNav() {
|
|
| 39 |
.toUpperCase() || "U";
|
| 40 |
|
| 41 |
return (
|
| 42 |
-
<
|
| 43 |
-
<
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
<
|
| 48 |
-
{
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
<
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
<
|
| 58 |
-
{user.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
<
|
| 65 |
-
|
| 66 |
-
<
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
<
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
| 85 |
);
|
| 86 |
}
|
|
|
|
| 15 |
import { LogOut, User, LayoutDashboard, Settings } from "lucide-react";
|
| 16 |
import Link from "next/link";
|
| 17 |
|
| 18 |
+
import { ModeToggle } from "@/components/mode-toggle";
|
| 19 |
+
|
| 20 |
export function UserNav() {
|
| 21 |
const { data: session } = useSession();
|
| 22 |
const router = useRouter();
|
|
|
|
| 41 |
.toUpperCase() || "U";
|
| 42 |
|
| 43 |
return (
|
| 44 |
+
<div className="flex items-center gap-3">
|
| 45 |
+
<ModeToggle />
|
| 46 |
+
<DropdownMenu>
|
| 47 |
+
<DropdownMenuTrigger asChild>
|
| 48 |
+
<Button variant="ghost" className="relative h-8 w-8 rounded-full hover:bg-transparent p-0">
|
| 49 |
+
<Avatar className="h-8 w-8 border-2 border-primary/50 transition-all hover:border-primary ring-2 ring-primary/10">
|
| 50 |
+
<AvatarImage src={user.image || ""} alt={user.name || "User"} />
|
| 51 |
+
<AvatarFallback className="bg-primary text-primary-foreground font-bold">
|
| 52 |
+
{initials}
|
| 53 |
+
</AvatarFallback>
|
| 54 |
+
</Avatar>
|
| 55 |
+
</Button>
|
| 56 |
+
</DropdownMenuTrigger>
|
| 57 |
+
<DropdownMenuContent className="w-56 glass border-white/10" align="end" forceMount>
|
| 58 |
+
<DropdownMenuLabel className="font-normal">
|
| 59 |
+
<div className="flex flex-col space-y-1">
|
| 60 |
+
<p className="text-sm font-medium leading-none">{user.name}</p>
|
| 61 |
+
<p className="text-xs leading-none text-muted-foreground">
|
| 62 |
+
{user.email}
|
| 63 |
+
</p>
|
| 64 |
+
</div>
|
| 65 |
+
</DropdownMenuLabel>
|
| 66 |
+
<DropdownMenuSeparator className="bg-white/10" />
|
| 67 |
+
<DropdownMenuItem asChild className="focus:bg-white/10 cursor-pointer">
|
| 68 |
+
<Link href="/dashboard" className="flex w-full items-center">
|
| 69 |
+
<LayoutDashboard className="mr-2 h-4 w-4" />
|
| 70 |
+
<span>Dashboard</span>
|
| 71 |
+
</Link>
|
| 72 |
+
</DropdownMenuItem>
|
| 73 |
+
<DropdownMenuItem asChild className="focus:bg-white/10 cursor-pointer">
|
| 74 |
+
<Link href="/dashboard/settings" className="flex w-full items-center">
|
| 75 |
+
<Settings className="mr-2 h-4 w-4" />
|
| 76 |
+
<span>Settings</span>
|
| 77 |
+
</Link>
|
| 78 |
+
</DropdownMenuItem>
|
| 79 |
+
<DropdownMenuSeparator className="bg-white/10" />
|
| 80 |
+
<DropdownMenuItem
|
| 81 |
+
onClick={handleSignOut}
|
| 82 |
+
className="focus:bg-red-500/10 text-red-500 focus:text-red-500 cursor-pointer"
|
| 83 |
+
>
|
| 84 |
+
<LogOut className="mr-2 h-4 w-4" />
|
| 85 |
+
<span>Log out</span>
|
| 86 |
+
</DropdownMenuItem>
|
| 87 |
+
</DropdownMenuContent>
|
| 88 |
+
</DropdownMenu>
|
| 89 |
+
</div>
|
| 90 |
);
|
| 91 |
}
|
src/components/mode-toggle.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Moon, Sun } from "lucide-react"
|
| 5 |
+
import { useTheme } from "next-themes"
|
| 6 |
+
|
| 7 |
+
import { Button } from "@/components/ui/button"
|
| 8 |
+
import {
|
| 9 |
+
DropdownMenu,
|
| 10 |
+
DropdownMenuContent,
|
| 11 |
+
DropdownMenuItem,
|
| 12 |
+
DropdownMenuTrigger,
|
| 13 |
+
} from "@/components/ui/dropdown-menu"
|
| 14 |
+
|
| 15 |
+
export function ModeToggle() {
|
| 16 |
+
const { setTheme } = useTheme()
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<DropdownMenu>
|
| 20 |
+
<DropdownMenuTrigger asChild>
|
| 21 |
+
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
|
| 22 |
+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
| 23 |
+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
| 24 |
+
<span className="sr-only">Toggle theme</span>
|
| 25 |
+
</Button>
|
| 26 |
+
</DropdownMenuTrigger>
|
| 27 |
+
<DropdownMenuContent align="end" className="glass border-white/10">
|
| 28 |
+
<DropdownMenuItem onClick={() => setTheme("light")} className="focus:bg-white/10 cursor-pointer">
|
| 29 |
+
Light
|
| 30 |
+
</DropdownMenuItem>
|
| 31 |
+
<DropdownMenuItem onClick={() => setTheme("dark")} className="focus:bg-white/10 cursor-pointer">
|
| 32 |
+
Dark
|
| 33 |
+
</DropdownMenuItem>
|
| 34 |
+
<DropdownMenuItem onClick={() => setTheme("system")} className="focus:bg-white/10 cursor-pointer">
|
| 35 |
+
System
|
| 36 |
+
</DropdownMenuItem>
|
| 37 |
+
</DropdownMenuContent>
|
| 38 |
+
</DropdownMenu>
|
| 39 |
+
)
|
| 40 |
+
}
|
src/features/analytics/components/key-metrics-row.tsx
CHANGED
|
@@ -60,19 +60,19 @@ export function KeyMetricsRow() {
|
|
| 60 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 61 |
{/* Active Items */}
|
| 62 |
<Link href="/dashboard/moderation?status=approved" className="block">
|
| 63 |
-
<Card className="glass
|
| 64 |
<CardHeader className="pb-3">
|
| 65 |
<div className="flex items-center justify-between">
|
| 66 |
-
<CardTitle className="text-sm font-medium text-
|
| 67 |
-
<Package className="h-4 w-4 text-
|
| 68 |
</div>
|
| 69 |
</CardHeader>
|
| 70 |
<CardContent>
|
| 71 |
-
<div className="text-3xl font-bold
|
| 72 |
{metricsData.activeItems}
|
| 73 |
</div>
|
| 74 |
<div className="flex items-center gap-1.5 mt-1">
|
| 75 |
-
<p className="text-xs text-
|
| 76 |
{metricsData.activeItemsTrend > 0 ? '+' : ''}{metricsData.activeItemsTrend} this week
|
| 77 |
</p>
|
| 78 |
</div>
|
|
@@ -82,10 +82,10 @@ export function KeyMetricsRow() {
|
|
| 82 |
|
| 83 |
{/* Untapped Opportunities */}
|
| 84 |
<Link href="/dashboard/requests" className="block">
|
| 85 |
-
<Card className="glass
|
| 86 |
<CardHeader className="pb-3">
|
| 87 |
<div className="flex items-center justify-between">
|
| 88 |
-
<CardTitle className="text-sm font-medium text-
|
| 89 |
<svg
|
| 90 |
xmlns="http://www.w3.org/2000/svg"
|
| 91 |
viewBox="0 0 24 24"
|
|
@@ -97,7 +97,7 @@ export function KeyMetricsRow() {
|
|
| 97 |
</div>
|
| 98 |
</CardHeader>
|
| 99 |
<CardContent>
|
| 100 |
-
<div className="text-3xl font-bold
|
| 101 |
{metricsData.pendingDemand}
|
| 102 |
</div>
|
| 103 |
<div className="flex items-center gap-1.5 mt-1">
|
|
@@ -106,7 +106,7 @@ export function KeyMetricsRow() {
|
|
| 106 |
+{metricsData.pendingDemandTrend} this week
|
| 107 |
</span>
|
| 108 |
)}
|
| 109 |
-
<p className="text-xs text-
|
| 110 |
review to monetize demand
|
| 111 |
</p>
|
| 112 |
</div>
|
|
@@ -115,13 +115,13 @@ export function KeyMetricsRow() {
|
|
| 115 |
</Link>
|
| 116 |
|
| 117 |
{/* Total Clicks */}
|
| 118 |
-
<Card className="glass
|
| 119 |
<CardHeader className="pb-3">
|
| 120 |
<div className="flex items-center justify-between">
|
| 121 |
-
<CardTitle className="text-sm font-medium text-
|
| 122 |
<svg
|
| 123 |
xmlns="http://www.w3.org/2000/svg"
|
| 124 |
-
className="h-4 w-4 text-
|
| 125 |
fill="none"
|
| 126 |
viewBox="0 0 24 24"
|
| 127 |
stroke="currentColor"
|
|
@@ -136,7 +136,7 @@ export function KeyMetricsRow() {
|
|
| 136 |
</div>
|
| 137 |
</CardHeader>
|
| 138 |
<CardContent>
|
| 139 |
-
<div className="text-3xl font-bold
|
| 140 |
{metricsData.totalClicks.toLocaleString()}
|
| 141 |
</div>
|
| 142 |
<div className="flex items-center gap-1.5 mt-1">
|
|
@@ -145,7 +145,7 @@ export function KeyMetricsRow() {
|
|
| 145 |
{metricsData.totalClicksTrend > 0 ? '↑' : '↓'} {Math.abs(metricsData.totalClicksTrend)}%
|
| 146 |
</span>
|
| 147 |
)}
|
| 148 |
-
<p className="text-xs text-
|
| 149 |
</div>
|
| 150 |
</CardContent>
|
| 151 |
</Card>
|
|
|
|
| 60 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 61 |
{/* Active Items */}
|
| 62 |
<Link href="/dashboard/moderation?status=approved" className="block">
|
| 63 |
+
<Card className="glass hover:bg-white/[0.07] dark:hover:bg-white/[0.07] transition-all duration-300 cursor-pointer hover:shadow-lg">
|
| 64 |
<CardHeader className="pb-3">
|
| 65 |
<div className="flex items-center justify-between">
|
| 66 |
+
<CardTitle className="text-sm font-medium text-muted-foreground">Active Items</CardTitle>
|
| 67 |
+
<Package className="h-4 w-4 text-muted-foreground" />
|
| 68 |
</div>
|
| 69 |
</CardHeader>
|
| 70 |
<CardContent>
|
| 71 |
+
<div className="text-3xl font-bold">
|
| 72 |
{metricsData.activeItems}
|
| 73 |
</div>
|
| 74 |
<div className="flex items-center gap-1.5 mt-1">
|
| 75 |
+
<p className="text-xs text-muted-foreground">
|
| 76 |
{metricsData.activeItemsTrend > 0 ? '+' : ''}{metricsData.activeItemsTrend} this week
|
| 77 |
</p>
|
| 78 |
</div>
|
|
|
|
| 82 |
|
| 83 |
{/* Untapped Opportunities */}
|
| 84 |
<Link href="/dashboard/requests" className="block">
|
| 85 |
+
<Card className="glass hover:bg-white/[0.07] dark:hover:bg-white/[0.07] transition-all duration-300 cursor-pointer hover:shadow-lg">
|
| 86 |
<CardHeader className="pb-3">
|
| 87 |
<div className="flex items-center justify-between">
|
| 88 |
+
<CardTitle className="text-sm font-medium text-muted-foreground">Untapped Opportunities</CardTitle>
|
| 89 |
<svg
|
| 90 |
xmlns="http://www.w3.org/2000/svg"
|
| 91 |
viewBox="0 0 24 24"
|
|
|
|
| 97 |
</div>
|
| 98 |
</CardHeader>
|
| 99 |
<CardContent>
|
| 100 |
+
<div className="text-3xl font-bold">
|
| 101 |
{metricsData.pendingDemand}
|
| 102 |
</div>
|
| 103 |
<div className="flex items-center gap-1.5 mt-1">
|
|
|
|
| 106 |
+{metricsData.pendingDemandTrend} this week
|
| 107 |
</span>
|
| 108 |
)}
|
| 109 |
+
<p className="text-xs text-muted-foreground">
|
| 110 |
review to monetize demand
|
| 111 |
</p>
|
| 112 |
</div>
|
|
|
|
| 115 |
</Link>
|
| 116 |
|
| 117 |
{/* Total Clicks */}
|
| 118 |
+
<Card className="glass hover:bg-white/[0.07] dark:hover:bg-white/[0.07] transition-all duration-300">
|
| 119 |
<CardHeader className="pb-3">
|
| 120 |
<div className="flex items-center justify-between">
|
| 121 |
+
<CardTitle className="text-sm font-medium text-muted-foreground">Total Clicks</CardTitle>
|
| 122 |
<svg
|
| 123 |
xmlns="http://www.w3.org/2000/svg"
|
| 124 |
+
className="h-4 w-4 text-muted-foreground"
|
| 125 |
fill="none"
|
| 126 |
viewBox="0 0 24 24"
|
| 127 |
stroke="currentColor"
|
|
|
|
| 136 |
</div>
|
| 137 |
</CardHeader>
|
| 138 |
<CardContent>
|
| 139 |
+
<div className="text-3xl font-bold">
|
| 140 |
{metricsData.totalClicks.toLocaleString()}
|
| 141 |
</div>
|
| 142 |
<div className="flex items-center gap-1.5 mt-1">
|
|
|
|
| 145 |
{metricsData.totalClicksTrend > 0 ? '↑' : '↓'} {Math.abs(metricsData.totalClicksTrend)}%
|
| 146 |
</span>
|
| 147 |
)}
|
| 148 |
+
<p className="text-xs text-muted-foreground">vs last week</p>
|
| 149 |
</div>
|
| 150 |
</CardContent>
|
| 151 |
</Card>
|
src/features/dashboard/components/video-list-row.tsx
CHANGED
|
@@ -47,9 +47,9 @@ export function VideoListRow({ video }: VideoListRowProps) {
|
|
| 47 |
const hasUnmetOpportunities = video.detectionCount > 0 && video.monetizedCount === 0;
|
| 48 |
|
| 49 |
return (
|
| 50 |
-
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 p-4 hover:bg-
|
| 51 |
{/* Thumbnail */}
|
| 52 |
-
<div className="relative shrink-0 w-full sm:w-40 aspect-video rounded-lg overflow-hidden border border-
|
| 53 |
{video.thumbnailUrl ? (
|
| 54 |
<img
|
| 55 |
src={video.thumbnailUrl}
|
|
@@ -57,7 +57,7 @@ export function VideoListRow({ video }: VideoListRowProps) {
|
|
| 57 |
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
| 58 |
/>
|
| 59 |
) : (
|
| 60 |
-
<div className="w-full h-full bg-
|
| 61 |
<Play className="h-8 w-8 text-muted-foreground" />
|
| 62 |
</div>
|
| 63 |
)}
|
|
@@ -103,7 +103,7 @@ export function VideoListRow({ video }: VideoListRowProps) {
|
|
| 103 |
{video.detectionCount} Detections
|
| 104 |
</Badge>
|
| 105 |
) : (
|
| 106 |
-
<Badge variant="secondary" className="bg-
|
| 107 |
<Clock className="h-3 w-3" />
|
| 108 |
{video.scanStatus === 'completed' ? 'Refine Mode' : 'Ready to Scan'}
|
| 109 |
</Badge>
|
|
@@ -112,7 +112,7 @@ export function VideoListRow({ video }: VideoListRowProps) {
|
|
| 112 |
</div>
|
| 113 |
|
| 114 |
{/* Engagement Sparkstats */}
|
| 115 |
-
<div className="flex items-center gap-4 sm:gap-6 shrink-0 w-full sm:w-auto pt-2 sm:pt-0 border-t sm:border-0 border-
|
| 116 |
<div className="flex flex-col items-center gap-0.5 group/stat">
|
| 117 |
<MousePointerClick className={cn("h-4 w-4 transition-colors", video.clickCount > 0 ? "text-primary" : "text-muted-foreground/30")} />
|
| 118 |
<span className="text-xs font-bold leading-none">{video.clickCount || 0}</span>
|
|
@@ -132,7 +132,7 @@ export function VideoListRow({ video }: VideoListRowProps) {
|
|
| 132 |
</div>
|
| 133 |
|
| 134 |
<div className="ml-auto sm:ml-4">
|
| 135 |
-
<Button asChild variant="outline" size="sm" className="h-8 rounded-lg
|
| 136 |
<Link href={`/showcase/${video.youtubeVideoId}`}>
|
| 137 |
Manage
|
| 138 |
<ExternalLink className="h-3 w-3" />
|
|
|
|
| 47 |
const hasUnmetOpportunities = video.detectionCount > 0 && video.monetizedCount === 0;
|
| 48 |
|
| 49 |
return (
|
| 50 |
+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 p-4 hover:bg-secondary/30 transition-colors group">
|
| 51 |
{/* Thumbnail */}
|
| 52 |
+
<div className="relative shrink-0 w-full sm:w-40 aspect-video rounded-lg overflow-hidden border border-border group-hover:border-primary/50 transition-colors">
|
| 53 |
{video.thumbnailUrl ? (
|
| 54 |
<img
|
| 55 |
src={video.thumbnailUrl}
|
|
|
|
| 57 |
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
| 58 |
/>
|
| 59 |
) : (
|
| 60 |
+
<div className="w-full h-full bg-secondary flex items-center justify-center">
|
| 61 |
<Play className="h-8 w-8 text-muted-foreground" />
|
| 62 |
</div>
|
| 63 |
)}
|
|
|
|
| 103 |
{video.detectionCount} Detections
|
| 104 |
</Badge>
|
| 105 |
) : (
|
| 106 |
+
<Badge variant="secondary" className="bg-secondary text-muted-foreground border-border text-[10px] gap-1 px-1.5 py-0 h-5">
|
| 107 |
<Clock className="h-3 w-3" />
|
| 108 |
{video.scanStatus === 'completed' ? 'Refine Mode' : 'Ready to Scan'}
|
| 109 |
</Badge>
|
|
|
|
| 112 |
</div>
|
| 113 |
|
| 114 |
{/* Engagement Sparkstats */}
|
| 115 |
+
<div className="flex items-center gap-4 sm:gap-6 shrink-0 w-full sm:w-auto pt-2 sm:pt-0 border-t sm:border-0 border-border/50 mt-2 sm:mt-0">
|
| 116 |
<div className="flex flex-col items-center gap-0.5 group/stat">
|
| 117 |
<MousePointerClick className={cn("h-4 w-4 transition-colors", video.clickCount > 0 ? "text-primary" : "text-muted-foreground/30")} />
|
| 118 |
<span className="text-xs font-bold leading-none">{video.clickCount || 0}</span>
|
|
|
|
| 132 |
</div>
|
| 133 |
|
| 134 |
<div className="ml-auto sm:ml-4">
|
| 135 |
+
<Button asChild variant="outline" size="sm" className="h-8 rounded-lg border-border hover:bg-secondary text-xs gap-1.5 transition-colors">
|
| 136 |
<Link href={`/showcase/${video.youtubeVideoId}`}>
|
| 137 |
Manage
|
| 138 |
<ExternalLink className="h-3 w-3" />
|
src/features/dashboard/components/video-workbench.tsx
CHANGED
|
@@ -120,9 +120,9 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 120 |
|
| 121 |
if (initialVideos.length === 0) {
|
| 122 |
return (
|
| 123 |
-
<Card className="border-dashed glass
|
| 124 |
<CardContent className="flex flex-col items-center justify-center p-12 text-center">
|
| 125 |
-
<div className="w-16 h-16 rounded-full bg-
|
| 126 |
<Youtube className="h-8 w-8 text-muted-foreground" />
|
| 127 |
</div>
|
| 128 |
<CardTitle className="text-2xl">No Videos Found</CardTitle>
|
|
@@ -154,28 +154,28 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 154 |
<p className="text-sm text-muted-foreground">Master list of your videos and their monetization performance.</p>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
-
<div className="flex flex-wrap items-center gap-4
|
| 158 |
-
<div className="px-3 border-r border-
|
| 159 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Total Clicks</p>
|
| 160 |
<p className="text-lg font-bold text-primary">{stats.totalClicks.toLocaleString()}</p>
|
| 161 |
</div>
|
| 162 |
-
<div className="px-3 border-r border-
|
| 163 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Fan Engagements</p>
|
| 164 |
<p className="text-lg font-bold text-amber-400">{stats.totalPledges.toLocaleString()}</p>
|
| 165 |
</div>
|
| 166 |
<div className="px-3">
|
| 167 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Monetized</p>
|
| 168 |
-
<p className="text-lg font-bold text-green-400">{stats.monetizedVideos} <span className="text-xs font-normal text-muted-foreground">/ {initialVideos.length}</span></p>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
-
<div className="px-2 border-l border-
|
| 172 |
<AlertDialog>
|
| 173 |
<AlertDialogTrigger asChild>
|
| 174 |
-
<Button variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:bg-
|
| 175 |
<Settings2 className="h-4 w-4" />
|
| 176 |
</Button>
|
| 177 |
</AlertDialogTrigger>
|
| 178 |
-
<AlertDialogContent className="glass-panel
|
| 179 |
<AlertDialogHeader>
|
| 180 |
<AlertDialogTitle>Account Connections</AlertDialogTitle>
|
| 181 |
<AlertDialogDescription>
|
|
@@ -185,9 +185,9 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 185 |
|
| 186 |
<div className="space-y-4 my-4">
|
| 187 |
{channels.map(channel => (
|
| 188 |
-
<div key={channel.id} className="flex items-center gap-3 p-3 rounded-xl bg-
|
| 189 |
{channel.thumbnailUrl && (
|
| 190 |
-
<img src={channel.thumbnailUrl} alt="" className="w-10 h-10 rounded-full border border-
|
| 191 |
)}
|
| 192 |
<div className="flex-1 min-w-0">
|
| 193 |
<p className="text-sm font-bold truncate">{channel.channelName}</p>
|
|
@@ -213,7 +213,7 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 213 |
Disconnect YouTube Account
|
| 214 |
</Button>
|
| 215 |
</div>
|
| 216 |
-
<AlertDialogCancel className="bg-
|
| 217 |
</AlertDialogFooter>
|
| 218 |
</AlertDialogContent>
|
| 219 |
</AlertDialog>
|
|
@@ -224,7 +224,7 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 224 |
{/* Controls Bar */}
|
| 225 |
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 py-2">
|
| 226 |
{/* Tabs */}
|
| 227 |
-
<div className="flex items-center gap-1 bg-
|
| 228 |
{(["all", "active", "opportunities", "requested"] as Tab[]).map(t => (
|
| 229 |
<button
|
| 230 |
key={t}
|
|
@@ -232,7 +232,7 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 232 |
className={cn(
|
| 233 |
"px-4 py-1.5 rounded-lg text-sm font-medium capitalize transition-all",
|
| 234 |
tab === t
|
| 235 |
-
? "bg-
|
| 236 |
: "text-muted-foreground hover:text-foreground"
|
| 237 |
)}
|
| 238 |
>
|
|
@@ -244,7 +244,7 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 244 |
<div className="flex flex-wrap items-center gap-3">
|
| 245 |
{/* Channel Filter (if multiple) */}
|
| 246 |
{channels.length > 1 && (
|
| 247 |
-
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-xl bg-
|
| 248 |
<Filter className="h-3.5 w-3.5 text-muted-foreground" />
|
| 249 |
<select
|
| 250 |
value={filterChannel}
|
|
@@ -277,8 +277,8 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 277 |
className={cn(
|
| 278 |
"flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all border",
|
| 279 |
sortBy === f.id
|
| 280 |
-
? "bg-primary/
|
| 281 |
-
: "bg-
|
| 282 |
)}
|
| 283 |
>
|
| 284 |
{f.icon}
|
|
@@ -293,8 +293,8 @@ export function VideoWorkbench({ initialVideos, channels, isGoogleLinked }: Vide
|
|
| 293 |
</div>
|
| 294 |
|
| 295 |
{/* Video List */}
|
| 296 |
-
<Card className="glass
|
| 297 |
-
<div className="divide-y divide-
|
| 298 |
{filteredAndSortedVideos.length > 0 ? (
|
| 299 |
filteredAndSortedVideos.map(video => (
|
| 300 |
<VideoListRow key={video.id} video={video} />
|
|
|
|
| 120 |
|
| 121 |
if (initialVideos.length === 0) {
|
| 122 |
return (
|
| 123 |
+
<Card className="border-dashed glass">
|
| 124 |
<CardContent className="flex flex-col items-center justify-center p-12 text-center">
|
| 125 |
+
<div className="w-16 h-16 rounded-full bg-secondary flex items-center justify-center mb-6 border border-border">
|
| 126 |
<Youtube className="h-8 w-8 text-muted-foreground" />
|
| 127 |
</div>
|
| 128 |
<CardTitle className="text-2xl">No Videos Found</CardTitle>
|
|
|
|
| 154 |
<p className="text-sm text-muted-foreground">Master list of your videos and their monetization performance.</p>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
+
<div className="flex flex-wrap items-center gap-4 glass p-2 rounded-xl backdrop-blur-sm">
|
| 158 |
+
<div className="px-3 border-r border-border last:border-0">
|
| 159 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Total Clicks</p>
|
| 160 |
<p className="text-lg font-bold text-primary">{stats.totalClicks.toLocaleString()}</p>
|
| 161 |
</div>
|
| 162 |
+
<div className="px-3 border-r border-border last:border-0">
|
| 163 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Fan Engagements</p>
|
| 164 |
<p className="text-lg font-bold text-amber-400">{stats.totalPledges.toLocaleString()}</p>
|
| 165 |
</div>
|
| 166 |
<div className="px-3">
|
| 167 |
<p className="text-[10px] uppercase font-bold text-muted-foreground tracking-widest mb-0.5">Monetized</p>
|
| 168 |
+
<p className="text-lg font-bold text-green-400 dark:text-green-400">{stats.monetizedVideos} <span className="text-xs font-normal text-muted-foreground">/ {initialVideos.length}</span></p>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
+
<div className="px-2 border-l border-border ml-2">
|
| 172 |
<AlertDialog>
|
| 173 |
<AlertDialogTrigger asChild>
|
| 174 |
+
<Button variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:bg-secondary rounded-full">
|
| 175 |
<Settings2 className="h-4 w-4" />
|
| 176 |
</Button>
|
| 177 |
</AlertDialogTrigger>
|
| 178 |
+
<AlertDialogContent className="glass-panel backdrop-blur-xl">
|
| 179 |
<AlertDialogHeader>
|
| 180 |
<AlertDialogTitle>Account Connections</AlertDialogTitle>
|
| 181 |
<AlertDialogDescription>
|
|
|
|
| 185 |
|
| 186 |
<div className="space-y-4 my-4">
|
| 187 |
{channels.map(channel => (
|
| 188 |
+
<div key={channel.id} className="flex items-center gap-3 p-3 rounded-xl bg-secondary border border-border">
|
| 189 |
{channel.thumbnailUrl && (
|
| 190 |
+
<img src={channel.thumbnailUrl} alt="" className="w-10 h-10 rounded-full border border-border" />
|
| 191 |
)}
|
| 192 |
<div className="flex-1 min-w-0">
|
| 193 |
<p className="text-sm font-bold truncate">{channel.channelName}</p>
|
|
|
|
| 213 |
Disconnect YouTube Account
|
| 214 |
</Button>
|
| 215 |
</div>
|
| 216 |
+
<AlertDialogCancel className="bg-secondary border-border hover:bg-muted transition-colors">Close</AlertDialogCancel>
|
| 217 |
</AlertDialogFooter>
|
| 218 |
</AlertDialogContent>
|
| 219 |
</AlertDialog>
|
|
|
|
| 224 |
{/* Controls Bar */}
|
| 225 |
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 py-2">
|
| 226 |
{/* Tabs */}
|
| 227 |
+
<div className="flex items-center gap-1 bg-secondary p-1 rounded-xl border border-border">
|
| 228 |
{(["all", "active", "opportunities", "requested"] as Tab[]).map(t => (
|
| 229 |
<button
|
| 230 |
key={t}
|
|
|
|
| 232 |
className={cn(
|
| 233 |
"px-4 py-1.5 rounded-lg text-sm font-medium capitalize transition-all",
|
| 234 |
tab === t
|
| 235 |
+
? "bg-background text-foreground shadow-sm border border-border/50"
|
| 236 |
: "text-muted-foreground hover:text-foreground"
|
| 237 |
)}
|
| 238 |
>
|
|
|
|
| 244 |
<div className="flex flex-wrap items-center gap-3">
|
| 245 |
{/* Channel Filter (if multiple) */}
|
| 246 |
{channels.length > 1 && (
|
| 247 |
+
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-xl bg-secondary border border-border">
|
| 248 |
<Filter className="h-3.5 w-3.5 text-muted-foreground" />
|
| 249 |
<select
|
| 250 |
value={filterChannel}
|
|
|
|
| 277 |
className={cn(
|
| 278 |
"flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold transition-all border",
|
| 279 |
sortBy === f.id
|
| 280 |
+
? "bg-primary/10 border-primary/20 text-primary"
|
| 281 |
+
: "bg-secondary border-border text-muted-foreground hover:border-muted-foreground/30"
|
| 282 |
)}
|
| 283 |
>
|
| 284 |
{f.icon}
|
|
|
|
| 293 |
</div>
|
| 294 |
|
| 295 |
{/* Video List */}
|
| 296 |
+
<Card className="glass overflow-hidden shadow-sm">
|
| 297 |
+
<div className="divide-y divide-border/30">
|
| 298 |
{filteredAndSortedVideos.length > 0 ? (
|
| 299 |
filteredAndSortedVideos.map(video => (
|
| 300 |
<VideoListRow key={video.id} video={video} />
|
src/features/discovery/components/youtube-connect-section.tsx
CHANGED
|
@@ -130,7 +130,7 @@ export function YouTubeConnectSection({
|
|
| 130 |
Disconnect Account
|
| 131 |
</Button>
|
| 132 |
</AlertDialogTrigger>
|
| 133 |
-
<AlertDialogContent className="glass-panel
|
| 134 |
<AlertDialogHeader>
|
| 135 |
<AlertDialogTitle>Disconnect YouTube Account?</AlertDialogTitle>
|
| 136 |
<AlertDialogDescription>
|
|
@@ -139,7 +139,7 @@ export function YouTubeConnectSection({
|
|
| 139 |
</AlertDialogDescription>
|
| 140 |
</AlertDialogHeader>
|
| 141 |
<AlertDialogFooter>
|
| 142 |
-
<AlertDialogCancel className="bg-
|
| 143 |
<AlertDialogAction
|
| 144 |
onClick={handleDisconnect}
|
| 145 |
className="bg-destructive text-white hover:bg-destructive/90 transition-colors"
|
|
@@ -151,7 +151,7 @@ export function YouTubeConnectSection({
|
|
| 151 |
</AlertDialog>
|
| 152 |
)}
|
| 153 |
{channels.length === 0 && (
|
| 154 |
-
<Button onClick={handleConnect} disabled={isConnecting} variant="outline" className="gap-2 glass hover:bg-
|
| 155 |
<Youtube className="h-4 w-4 text-red-600" />
|
| 156 |
Connect YouTube
|
| 157 |
</Button>
|
|
@@ -159,9 +159,9 @@ export function YouTubeConnectSection({
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
{channels.length === 0 ? (
|
| 162 |
-
<Card className="border-dashed glass bg-
|
| 163 |
<CardHeader className="text-center py-10">
|
| 164 |
-
<div className="mx-auto w-12 h-12 rounded-full bg-
|
| 165 |
<Youtube className="h-6 w-6 text-red-500" />
|
| 166 |
</div>
|
| 167 |
<CardTitle>No YouTube Channels Connected</CardTitle>
|
|
@@ -194,14 +194,14 @@ export function YouTubeConnectSection({
|
|
| 194 |
alt={channel.channelName}
|
| 195 |
width={48}
|
| 196 |
height={48}
|
| 197 |
-
className="w-12 h-12 rounded-full border border-
|
| 198 |
/>
|
| 199 |
-
<div className="absolute -bottom-1 -right-1 bg-background rounded-full p-0.5 border border-
|
| 200 |
<Youtube className="h-3 w-3 text-red-600" />
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
) : (
|
| 204 |
-
<div className="w-12 h-12 rounded-full bg-
|
| 205 |
<Youtube className="h-6 w-6 text-red-600" />
|
| 206 |
</div>
|
| 207 |
)}
|
|
@@ -222,7 +222,7 @@ export function YouTubeConnectSection({
|
|
| 222 |
{status.label}
|
| 223 |
</Badge>
|
| 224 |
</CardHeader>
|
| 225 |
-
<CardContent className="pt-0 flex justify-end items-center bg-
|
| 226 |
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 opacity-70 hover:opacity-100 transition-opacity" asChild>
|
| 227 |
<a href={`https://youtube.com/channel/${channel.channelId}`} target="_blank" rel="noopener noreferrer">
|
| 228 |
View <ExternalLink className="h-3 w-3" />
|
|
|
|
| 130 |
Disconnect Account
|
| 131 |
</Button>
|
| 132 |
</AlertDialogTrigger>
|
| 133 |
+
<AlertDialogContent className="glass-panel backdrop-blur-xl">
|
| 134 |
<AlertDialogHeader>
|
| 135 |
<AlertDialogTitle>Disconnect YouTube Account?</AlertDialogTitle>
|
| 136 |
<AlertDialogDescription>
|
|
|
|
| 139 |
</AlertDialogDescription>
|
| 140 |
</AlertDialogHeader>
|
| 141 |
<AlertDialogFooter>
|
| 142 |
+
<AlertDialogCancel className="bg-secondary border-border hover:bg-muted transition-colors">Cancel</AlertDialogCancel>
|
| 143 |
<AlertDialogAction
|
| 144 |
onClick={handleDisconnect}
|
| 145 |
className="bg-destructive text-white hover:bg-destructive/90 transition-colors"
|
|
|
|
| 151 |
</AlertDialog>
|
| 152 |
)}
|
| 153 |
{channels.length === 0 && (
|
| 154 |
+
<Button onClick={handleConnect} disabled={isConnecting} variant="outline" className="gap-2 glass hover:bg-secondary transition-all">
|
| 155 |
<Youtube className="h-4 w-4 text-red-600" />
|
| 156 |
Connect YouTube
|
| 157 |
</Button>
|
|
|
|
| 159 |
</div>
|
| 160 |
|
| 161 |
{channels.length === 0 ? (
|
| 162 |
+
<Card className="border-dashed glass bg-secondary/30 border-border transition-all hover:bg-secondary/50">
|
| 163 |
<CardHeader className="text-center py-10">
|
| 164 |
+
<div className="mx-auto w-12 h-12 rounded-full bg-secondary flex items-center justify-center mb-4 border border-border">
|
| 165 |
<Youtube className="h-6 w-6 text-red-500" />
|
| 166 |
</div>
|
| 167 |
<CardTitle>No YouTube Channels Connected</CardTitle>
|
|
|
|
| 194 |
alt={channel.channelName}
|
| 195 |
width={48}
|
| 196 |
height={48}
|
| 197 |
+
className="w-12 h-12 rounded-full border border-border shadow-lg object-cover group-hover:scale-105 transition-transform"
|
| 198 |
/>
|
| 199 |
+
<div className="absolute -bottom-1 -right-1 bg-background rounded-full p-0.5 border border-border">
|
| 200 |
<Youtube className="h-3 w-3 text-red-600" />
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
) : (
|
| 204 |
+
<div className="w-12 h-12 rounded-full bg-secondary flex items-center justify-center border border-border">
|
| 205 |
<Youtube className="h-6 w-6 text-red-600" />
|
| 206 |
</div>
|
| 207 |
)}
|
|
|
|
| 222 |
{status.label}
|
| 223 |
</Badge>
|
| 224 |
</CardHeader>
|
| 225 |
+
<CardContent className="pt-0 flex justify-end items-center bg-secondary/30 px-6 py-3 border-t border-border">
|
| 226 |
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 opacity-70 hover:opacity-100 transition-opacity" asChild>
|
| 227 |
<a href={`https://youtube.com/channel/${channel.channelId}`} target="_blank" rel="noopener noreferrer">
|
| 228 |
View <ExternalLink className="h-3 w-3" />
|
src/features/vault/components/affiliate-submission-dialog.tsx
CHANGED
|
@@ -92,7 +92,7 @@ export function AffiliateSubmissionDialog({
|
|
| 92 |
</Button>
|
| 93 |
)}
|
| 94 |
</DialogTrigger>
|
| 95 |
-
<DialogContent className="sm:max-w-[500px] glass
|
| 96 |
<DialogHeader>
|
| 97 |
<DialogTitle className="text-xl font-bold tracking-tight">Submit Product Link</DialogTitle>
|
| 98 |
<DialogDescription className="text-muted-foreground">
|
|
@@ -107,7 +107,7 @@ export function AffiliateSubmissionDialog({
|
|
| 107 |
placeholder="e.g. Sony A7 III Camera"
|
| 108 |
value={formData.productName}
|
| 109 |
onChange={(e) => setFormData({ ...formData, productName: e.target.value })}
|
| 110 |
-
className="bg-
|
| 111 |
required
|
| 112 |
/>
|
| 113 |
</div>
|
|
@@ -119,7 +119,7 @@ export function AffiliateSubmissionDialog({
|
|
| 119 |
placeholder="https://amazon.com/..."
|
| 120 |
value={formData.productUrl}
|
| 121 |
onChange={(e) => setFormData({ ...formData, productUrl: e.target.value })}
|
| 122 |
-
className="bg-
|
| 123 |
required
|
| 124 |
/>
|
| 125 |
</div>
|
|
@@ -130,7 +130,7 @@ export function AffiliateSubmissionDialog({
|
|
| 130 |
placeholder="https://amzn.to/..."
|
| 131 |
value={formData.affiliateUrl}
|
| 132 |
onChange={(e) => setFormData({ ...formData, affiliateUrl: e.target.value })}
|
| 133 |
-
className="bg-
|
| 134 |
required
|
| 135 |
/>
|
| 136 |
</div>
|
|
@@ -142,7 +142,7 @@ export function AffiliateSubmissionDialog({
|
|
| 142 |
placeholder="e.g. This is the exact model used in the video..."
|
| 143 |
value={formData.note}
|
| 144 |
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
| 145 |
-
className="min-h-[80px] bg-
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
<div className="grid grid-cols-2 gap-4">
|
|
@@ -153,7 +153,7 @@ export function AffiliateSubmissionDialog({
|
|
| 153 |
placeholder="Alex"
|
| 154 |
value={formData.submitterName}
|
| 155 |
onChange={(e) => setFormData({ ...formData, submitterName: e.target.value })}
|
| 156 |
-
className="bg-
|
| 157 |
/>
|
| 158 |
</div>
|
| 159 |
<div className="space-y-2">
|
|
@@ -164,7 +164,7 @@ export function AffiliateSubmissionDialog({
|
|
| 164 |
placeholder="alex@example.com"
|
| 165 |
value={formData.submitterEmail}
|
| 166 |
onChange={(e) => setFormData({ ...formData, submitterEmail: e.target.value })}
|
| 167 |
-
className="bg-
|
| 168 |
/>
|
| 169 |
</div>
|
| 170 |
</div>
|
|
|
|
| 92 |
</Button>
|
| 93 |
)}
|
| 94 |
</DialogTrigger>
|
| 95 |
+
<DialogContent className="sm:max-w-[500px] glass-panel">
|
| 96 |
<DialogHeader>
|
| 97 |
<DialogTitle className="text-xl font-bold tracking-tight">Submit Product Link</DialogTitle>
|
| 98 |
<DialogDescription className="text-muted-foreground">
|
|
|
|
| 107 |
placeholder="e.g. Sony A7 III Camera"
|
| 108 |
value={formData.productName}
|
| 109 |
onChange={(e) => setFormData({ ...formData, productName: e.target.value })}
|
| 110 |
+
className="bg-secondary border-border"
|
| 111 |
required
|
| 112 |
/>
|
| 113 |
</div>
|
|
|
|
| 119 |
placeholder="https://amazon.com/..."
|
| 120 |
value={formData.productUrl}
|
| 121 |
onChange={(e) => setFormData({ ...formData, productUrl: e.target.value })}
|
| 122 |
+
className="bg-secondary border-border"
|
| 123 |
required
|
| 124 |
/>
|
| 125 |
</div>
|
|
|
|
| 130 |
placeholder="https://amzn.to/..."
|
| 131 |
value={formData.affiliateUrl}
|
| 132 |
onChange={(e) => setFormData({ ...formData, affiliateUrl: e.target.value })}
|
| 133 |
+
className="bg-secondary border-border"
|
| 134 |
required
|
| 135 |
/>
|
| 136 |
</div>
|
|
|
|
| 142 |
placeholder="e.g. This is the exact model used in the video..."
|
| 143 |
value={formData.note}
|
| 144 |
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
| 145 |
+
className="min-h-[80px] bg-secondary border-border"
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
<div className="grid grid-cols-2 gap-4">
|
|
|
|
| 153 |
placeholder="Alex"
|
| 154 |
value={formData.submitterName}
|
| 155 |
onChange={(e) => setFormData({ ...formData, submitterName: e.target.value })}
|
| 156 |
+
className="bg-secondary border-border"
|
| 157 |
/>
|
| 158 |
</div>
|
| 159 |
<div className="space-y-2">
|
|
|
|
| 164 |
placeholder="alex@example.com"
|
| 165 |
value={formData.submitterEmail}
|
| 166 |
onChange={(e) => setFormData({ ...formData, submitterEmail: e.target.value })}
|
| 167 |
+
className="bg-secondary border-border"
|
| 168 |
/>
|
| 169 |
</div>
|
| 170 |
</div>
|
src/features/vault/components/product-request-dialog.tsx
CHANGED
|
@@ -151,12 +151,12 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 151 |
return (
|
| 152 |
<Dialog open={open} onOpenChange={setOpen}>
|
| 153 |
<DialogTrigger asChild>
|
| 154 |
-
<Button variant="outline" className="rounded-full gap-2 border-primary/20 hover:border-primary/40 hover:bg-primary/5 transition-all
|
| 155 |
<HelpCircle className="w-4 h-4" />
|
| 156 |
Can't find a product?
|
| 157 |
</Button>
|
| 158 |
</DialogTrigger>
|
| 159 |
-
<DialogContent className="sm:max-w-[540px] glass
|
| 160 |
<DialogHeader>
|
| 161 |
<DialogTitle className="text-xl font-bold tracking-tight">Request a Product</DialogTitle>
|
| 162 |
<DialogDescription className="text-muted-foreground">
|
|
@@ -168,10 +168,10 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 168 |
<div className="space-y-2">
|
| 169 |
<Label htmlFor="video-select" className="text-sm font-medium">Which video did you see it in?*</Label>
|
| 170 |
<Select value={selectedVideoId} onValueChange={setSelectedVideoId}>
|
| 171 |
-
<SelectTrigger className="bg-
|
| 172 |
<SelectValue placeholder="Select a video" />
|
| 173 |
</SelectTrigger>
|
| 174 |
-
<SelectContent className="bg-
|
| 175 |
{videos.map(v => (
|
| 176 |
<SelectItem key={v.id} value={v.id}>{v.title}</SelectItem>
|
| 177 |
))}
|
|
@@ -186,7 +186,7 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 186 |
placeholder="e.g. The blue jacket at 04:20..."
|
| 187 |
value={formData.note}
|
| 188 |
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
| 189 |
-
className="min-h-[80px] bg-
|
| 190 |
required
|
| 191 |
/>
|
| 192 |
</div>
|
|
@@ -198,7 +198,7 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 198 |
placeholder="Alex"
|
| 199 |
value={formData.name}
|
| 200 |
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
| 201 |
-
className="bg-
|
| 202 |
/>
|
| 203 |
</div>
|
| 204 |
<div className="space-y-2">
|
|
@@ -209,7 +209,7 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 209 |
placeholder="alex@gmail.com"
|
| 210 |
value={formData.email}
|
| 211 |
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
| 212 |
-
className="bg-
|
| 213 |
/>
|
| 214 |
</div>
|
| 215 |
</div>
|
|
@@ -224,7 +224,7 @@ export function ProductRequestDialog({ videoId: initialVideoId, creatorId, video
|
|
| 224 |
aspectRatio="video"
|
| 225 |
placeholderText="Paste image link or upload a screenshot"
|
| 226 |
extraLabel={
|
| 227 |
-
<span className="ml-auto text-[10px] font-normal text-muted-foreground bg-
|
| 228 |
Cmd+V to Paste
|
| 229 |
</span>
|
| 230 |
}
|
|
|
|
| 151 |
return (
|
| 152 |
<Dialog open={open} onOpenChange={setOpen}>
|
| 153 |
<DialogTrigger asChild>
|
| 154 |
+
<Button variant="outline" className="rounded-full gap-2 border-primary/20 hover:border-primary/40 hover:bg-primary/5 transition-all">
|
| 155 |
<HelpCircle className="w-4 h-4" />
|
| 156 |
Can't find a product?
|
| 157 |
</Button>
|
| 158 |
</DialogTrigger>
|
| 159 |
+
<DialogContent className="sm:max-w-[540px] glass-panel">
|
| 160 |
<DialogHeader>
|
| 161 |
<DialogTitle className="text-xl font-bold tracking-tight">Request a Product</DialogTitle>
|
| 162 |
<DialogDescription className="text-muted-foreground">
|
|
|
|
| 168 |
<div className="space-y-2">
|
| 169 |
<Label htmlFor="video-select" className="text-sm font-medium">Which video did you see it in?*</Label>
|
| 170 |
<Select value={selectedVideoId} onValueChange={setSelectedVideoId}>
|
| 171 |
+
<SelectTrigger className="bg-secondary border-border focus:border-primary/50">
|
| 172 |
<SelectValue placeholder="Select a video" />
|
| 173 |
</SelectTrigger>
|
| 174 |
+
<SelectContent className="bg-popover border-border">
|
| 175 |
{videos.map(v => (
|
| 176 |
<SelectItem key={v.id} value={v.id}>{v.title}</SelectItem>
|
| 177 |
))}
|
|
|
|
| 186 |
placeholder="e.g. The blue jacket at 04:20..."
|
| 187 |
value={formData.note}
|
| 188 |
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
| 189 |
+
className="min-h-[80px] bg-secondary border-border focus:border-primary/50 placeholder:text-muted-foreground/50"
|
| 190 |
required
|
| 191 |
/>
|
| 192 |
</div>
|
|
|
|
| 198 |
placeholder="Alex"
|
| 199 |
value={formData.name}
|
| 200 |
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
| 201 |
+
className="bg-secondary border-border placeholder:text-muted-foreground/50"
|
| 202 |
/>
|
| 203 |
</div>
|
| 204 |
<div className="space-y-2">
|
|
|
|
| 209 |
placeholder="alex@gmail.com"
|
| 210 |
value={formData.email}
|
| 211 |
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
| 212 |
+
className="bg-secondary border-border placeholder:text-muted-foreground/50"
|
| 213 |
/>
|
| 214 |
</div>
|
| 215 |
</div>
|
|
|
|
| 224 |
aspectRatio="video"
|
| 225 |
placeholderText="Paste image link or upload a screenshot"
|
| 226 |
extraLabel={
|
| 227 |
+
<span className="ml-auto text-[10px] font-normal text-muted-foreground bg-secondary px-1.5 py-0.5 rounded border border-border animate-pulse">
|
| 228 |
Cmd+V to Paste
|
| 229 |
</span>
|
| 230 |
}
|
src/features/vault/components/quick-analyze-tool.tsx
CHANGED
|
@@ -38,40 +38,39 @@ export function QuickAnalyzeTool() {
|
|
| 38 |
};
|
| 39 |
|
| 40 |
return (
|
| 41 |
-
<Card className="glass
|
| 42 |
-
<
|
| 43 |
-
<
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
<CardTitle className="flex items-center gap-2">
|
| 47 |
-
<Sparkles className="h-5 w-5 text-amber-400" />
|
| 48 |
-
Quick Analyze
|
| 49 |
-
</CardTitle>
|
| 50 |
-
<CardDescription>
|
| 51 |
-
Paste a YouTube link to instantly analyze and showcase products from the video.
|
| 52 |
-
</CardDescription>
|
| 53 |
-
</CardHeader>
|
| 54 |
-
<CardContent>
|
| 55 |
-
<form onSubmit={handleSubmit} className="space-y-4">
|
| 56 |
-
<div className="flex gap-2">
|
| 57 |
<Input
|
| 58 |
-
placeholder="
|
| 59 |
value={url}
|
| 60 |
onChange={(e) => setUrl(e.target.value)}
|
| 61 |
-
className="bg-
|
| 62 |
disabled={loading}
|
| 63 |
/>
|
| 64 |
-
<Button type="submit" disabled={loading || !url}>
|
| 65 |
-
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Analyze"}
|
| 66 |
-
</Button>
|
| 67 |
</div>
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</CardContent>
|
| 76 |
</Card>
|
| 77 |
);
|
|
|
|
| 38 |
};
|
| 39 |
|
| 40 |
return (
|
| 41 |
+
<Card className="glass overflow-hidden shadow-sm hover:shadow-md transition-all duration-300">
|
| 42 |
+
<CardContent className="p-4">
|
| 43 |
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row items-center gap-3">
|
| 44 |
+
<div className="relative flex-1 w-full">
|
| 45 |
+
<Sparkles className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-amber-400/80" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
<Input
|
| 47 |
+
placeholder="Add any YouTube video to your Vault..."
|
| 48 |
value={url}
|
| 49 |
onChange={(e) => setUrl(e.target.value)}
|
| 50 |
+
className="pl-10 h-11 bg-secondary/50 border-border focus:border-primary/50 transition-all font-medium"
|
| 51 |
disabled={loading}
|
| 52 |
/>
|
|
|
|
|
|
|
|
|
|
| 53 |
</div>
|
| 54 |
+
<Button
|
| 55 |
+
type="submit"
|
| 56 |
+
disabled={loading || !url}
|
| 57 |
+
className="h-11 px-6 shadow-sm hover:translate-y-[-1px] active:translate-y-[0px] transition-all"
|
| 58 |
+
>
|
| 59 |
+
{loading ? (
|
| 60 |
+
<Loader2 className="h-4 w-4 animate-spin" />
|
| 61 |
+
) : (
|
| 62 |
+
<span className="flex items-center gap-2">
|
| 63 |
+
Add to Vault
|
| 64 |
+
</span>
|
| 65 |
+
)}
|
| 66 |
+
</Button>
|
| 67 |
</form>
|
| 68 |
+
{error && (
|
| 69 |
+
<div className="mt-3 flex items-center gap-2 text-xs text-destructive bg-destructive/5 p-2 rounded-lg border border-destructive/10 animate-in slide-in-from-top-1">
|
| 70 |
+
<AlertCircle className="h-3.5 w-3.5" />
|
| 71 |
+
<p>{error}</p>
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
| 74 |
</CardContent>
|
| 75 |
</Card>
|
| 76 |
);
|