@lucifer91299/create-portal-app 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ## Quick start
12
12
 
13
13
  ```bash
14
- npx create-portal-app my-portal
14
+ npx @lucifer91299/create-portal-app my-portal
15
15
  ```
16
16
 
17
17
  The CLI walks you through each choice with arrow-key menus, then generates a ready-to-run project.
@@ -31,23 +31,23 @@ npm run dev
31
31
  Skip every prompt and use defaults instantly:
32
32
 
33
33
  ```bash
34
- npx create-portal-app my-portal --yes
34
+ npx @lucifer91299/create-portal-app my-portal --yes
35
35
  ```
36
36
 
37
37
  Override specific options while skipping the rest:
38
38
 
39
39
  ```bash
40
40
  # Change login style and sidebar
41
- npx create-portal-app my-portal --yes --login=simple --sidebar=rail
41
+ npx @lucifer91299/create-portal-app my-portal --yes --login=simple --sidebar=rail
42
42
 
43
43
  # Custom brand colours
44
- npx create-portal-app my-portal --yes --primary=#E11D48 --accent=#F59E0B --success=#10B981
44
+ npx @lucifer91299/create-portal-app my-portal --yes --primary=#E11D48 --accent=#F59E0B --success=#10B981
45
45
 
46
46
  # Laravel backend
47
- npx create-portal-app my-portal --yes --auth=laravel
47
+ npx @lucifer91299/create-portal-app my-portal --yes --auth=laravel
48
48
 
49
49
  # Show all flags
50
- npx create-portal-app --help
50
+ npx @lucifer91299/create-portal-app --help
51
51
  ```
52
52
 
53
53
  ---
@@ -198,7 +198,7 @@ Clean gradient card login with optional role-select splash. Best for SaaS or min
198
198
  ## Local development (use a local SDK build)
199
199
 
200
200
  ```bash
201
- npx create-portal-app my-portal --yes --local-ui=../../packages/ui
201
+ npx @lucifer91299/create-portal-app my-portal --yes --local-ui=../../packages/ui
202
202
  # generates: "@lucifer91299/ui": "file:../../packages/ui" in package.json
203
203
  ```
204
204
 
package/dist/cli/index.js CHANGED
@@ -399,7 +399,7 @@ export default api
399
399
  `;
400
400
  }
401
401
  function genNavConfig(o) {
402
- return `import { LayoutDashboard, Settings, User } from 'lucide-react'
402
+ return `import { LayoutDashboard, Settings, Users, FileText } from 'lucide-react'
403
403
  import type { NavGroup } from '@lucifer91299/ui'
404
404
 
405
405
  export const navGroups: NavGroup[] = [
@@ -407,14 +407,14 @@ export const navGroups: NavGroup[] = [
407
407
  heading: 'Main',
408
408
  groupIcon: <LayoutDashboard className="h-3.5 w-3.5" />,
409
409
  items: [
410
- { label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> },
410
+ { label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> },
411
+ { label: 'Users', href: '/dashboard/users', icon: <Users className="h-4 w-4" /> },
411
412
  ],
412
413
  },
413
414
  {
414
415
  heading: 'Account',
415
- groupIcon: <User className="h-3.5 w-3.5" />,
416
+ groupIcon: <Settings className="h-3.5 w-3.5" />,
416
417
  items: [
417
- { label: 'Profile', href: '/dashboard/profile', icon: <User className="h-4 w-4" /> },
418
418
  { label: 'Settings', href: '/dashboard/settings', icon: <Settings className="h-4 w-4" /> },
419
419
  ],
420
420
  },
@@ -424,14 +424,21 @@ export const navGroups: NavGroup[] = [
424
424
  function genDashboardLayout(o) {
425
425
  return `'use client'
426
426
 
427
- import { DashboardLayout } from '@lucifer91299/ui'
428
- import { use${o.authMode === "multi-role" ? "MultiRoleAuth" : "JwtAuth"} } from '@lucifer91299/ui'
427
+ import { DashboardLayout, PageFooter, use${o.authMode === "multi-role" ? "MultiRoleAuth" : "JwtAuth"} } from '@lucifer91299/ui'
429
428
  import { usePathname } from 'next/navigation'
430
429
  import { navGroups } from '@/components/layout/nav-config'
431
430
 
432
431
  export default function Layout({ children }: { children: React.ReactNode }) {
433
432
  const pathname = usePathname()
434
- const { user, logout } = use${o.authMode === "multi-role" ? "MultiRoleAuth({ roles: [] })" : "JwtAuth()"}
433
+ const { user, loading, logout } = use${o.authMode === "multi-role" ? "MultiRoleAuth({ roles: [] })" : "JwtAuth()"}
434
+
435
+ if (loading) {
436
+ return (
437
+ <div className="min-h-screen flex items-center justify-center bg-surface-secondary">
438
+ <div className="w-8 h-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
439
+ </div>
440
+ )
441
+ }
435
442
 
436
443
  return (
437
444
  <DashboardLayout
@@ -439,11 +446,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
439
446
  sidebar="${o.sidebarStyle}"
440
447
  projectName="${o.projectName}"
441
448
  logoSrc="/brand/logo.svg"
442
- user={{ name: (user as any)?.name ?? 'User', role: (user as any)?.role ?? '' }}
449
+ user={{ name: String((user as any)?.name ?? 'User'), role: String((user as any)?.role ?? '') }}
443
450
  pathname={pathname}
444
451
  onLogout={logout}
445
452
  >
446
- {children}
453
+ <div className="flex flex-col min-h-full">
454
+ <div className="flex-1">{children}</div>
455
+ <PageFooter
456
+ organizationName="${o.projectName}"
457
+ logoSrc="/brand/logo.svg"
458
+ poweredByText="Powered by"
459
+ poweredByHref="#"
460
+ />
461
+ </div>
447
462
  </DashboardLayout>
448
463
  )
449
464
  }
@@ -508,94 +523,260 @@ export default function Home() {
508
523
  `;
509
524
  }
510
525
  function genDashboardHomePage(o) {
511
- return `import { TrendingUp, Users, ShoppingCart, Activity } from 'lucide-react'
526
+ return `'use client'
527
+
528
+ import { PageShell, DataTable, StatusBadge, useJwtAuth } from '@lucifer91299/ui'
529
+ import { TrendingUp, Users, ShoppingCart, Activity } from 'lucide-react'
512
530
 
513
531
  const stats = [
514
- { label: 'Total Users', value: '2,847', change: '+12%', icon: Users },
515
- { label: 'Revenue', value: '\u20B948,295', change: '+8.2%', icon: TrendingUp },
516
- { label: 'Orders', value: '1,429', change: '+5.1%', icon: ShoppingCart },
517
- { label: 'Active Now', value: '94', change: '+3', icon: Activity },
532
+ { label: 'Total Users', value: '2,847', change: '+12%', icon: Users, color: 'bg-blue-50 text-blue-600' },
533
+ { label: 'Revenue', value: '\u20B948,295', change: '+8.2%', icon: TrendingUp, color: 'bg-green-50 text-green-600' },
534
+ { label: 'Orders', value: '1,429', change: '+5.1%', icon: ShoppingCart, color: 'bg-orange-50 text-orange-600' },
535
+ { label: 'Active Now', value: '94', change: '+3', icon: Activity, color: 'bg-purple-50 text-purple-600' },
518
536
  ]
519
537
 
520
538
  const activity = [
521
- { id: 'ORD-001', user: 'Rahul Sharma', action: 'New order placed', time: '2 min ago', status: 'Pending' },
522
- { id: 'ORD-002', user: 'Priya Mehta', action: 'Payment received', time: '14 min ago', status: 'Paid' },
523
- { id: 'ORD-003', user: 'Amit Patel', action: 'Order approved', time: '1 hr ago', status: 'Approved' },
524
- { id: 'ORD-004', user: 'Sneha Iyer', action: 'Account registered', time: '3 hr ago', status: 'Active' },
525
- { id: 'ORD-005', user: 'Vikram Singh', action: 'Order completed', time: 'Yesterday', status: 'Completed' },
539
+ { id: 'ORD-001', user: 'Rahul Sharma', action: 'New order placed', time: '2 min ago', status: 'pending' },
540
+ { id: 'ORD-002', user: 'Priya Mehta', action: 'Payment received', time: '14 min ago', status: 'paid' },
541
+ { id: 'ORD-003', user: 'Amit Patel', action: 'Order approved', time: '1 hr ago', status: 'approved' },
542
+ { id: 'ORD-004', user: 'Sneha Iyer', action: 'Account registered', time: '3 hr ago', status: 'active' },
543
+ { id: 'ORD-005', user: 'Vikram Singh', action: 'Order completed', time: 'Yesterday', status: 'completed' },
526
544
  ]
527
545
 
528
- const statusColors: Record<string, string> = {
529
- Pending: 'bg-yellow-100 text-yellow-800',
530
- Paid: 'bg-blue-100 text-blue-800',
531
- Approved: 'bg-green-100 text-green-800',
532
- Active: 'bg-purple-100 text-purple-800',
533
- Completed: 'bg-gray-100 text-gray-700',
534
- }
535
-
536
546
  export default function DashboardHome() {
547
+ const { user } = useJwtAuth()
548
+
537
549
  return (
538
550
  <div className="p-6 space-y-6">
539
- <div className="page-header">
540
- <h1 className="page-title">${o.projectName}</h1>
541
- <p className="text-body text-label-secondary">Welcome back, Admin</p>
542
- </div>
551
+ <PageShell
552
+ title={\`Welcome back, \${String(user?.name ?? 'Admin')}\`}
553
+ subtitle="Here's what's happening today."
554
+ />
543
555
 
544
556
  <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
545
- {stats.map(({ label, value, change, icon: Icon }) => (
546
- <div key={label} className="card p-5">
557
+ {stats.map(({ label, value, change, icon: Icon, color }) => (
558
+ <div key={label} className="bg-white rounded-xl p-5 shadow-sm border border-separator">
547
559
  <div className="flex items-center justify-between mb-3">
548
560
  <p className="text-subhead text-label-secondary">{label}</p>
549
- <div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
550
- <Icon className="w-4 h-4 text-primary" />
561
+ <div className={\`w-8 h-8 rounded-lg flex items-center justify-center \${color}\`}>
562
+ <Icon className="w-4 h-4" />
551
563
  </div>
552
564
  </div>
553
565
  <p className="text-title1 font-bold text-label-primary">{value}</p>
554
- <p className="text-footnote text-green-600 mt-1 font-medium">{change} this month</p>
566
+ <p className="text-footnote text-green-600 mt-1">{change} this month</p>
555
567
  </div>
556
568
  ))}
557
569
  </div>
558
570
 
559
- <div className="card overflow-hidden">
560
- <div className="px-5 py-4 border-b border-gray-100">
571
+ <div className="bg-white rounded-xl shadow-sm border border-separator overflow-hidden">
572
+ <div className="px-5 py-4 border-b border-separator">
561
573
  <h2 className="text-callout font-semibold text-label-primary">Recent Activity</h2>
562
574
  </div>
563
- <table className="w-full text-sm">
564
- <thead className="bg-gray-50 text-label-secondary text-subhead">
565
- <tr>
566
- <th className="px-5 py-3 text-left font-medium">ID</th>
567
- <th className="px-5 py-3 text-left font-medium">User</th>
568
- <th className="px-5 py-3 text-left font-medium">Action</th>
569
- <th className="px-5 py-3 text-left font-medium">Time</th>
570
- <th className="px-5 py-3 text-left font-medium">Status</th>
571
- </tr>
572
- </thead>
573
- <tbody className="divide-y divide-gray-50">
574
- {activity.map((row) => (
575
- <tr key={row.id} className="hover:bg-gray-50 transition-colors">
576
- <td className="px-5 py-3.5 font-mono text-xs text-label-secondary">{row.id}</td>
577
- <td className="px-5 py-3.5 font-medium text-label-primary">{row.user}</td>
578
- <td className="px-5 py-3.5 text-label-secondary">{row.action}</td>
579
- <td className="px-5 py-3.5 text-label-tertiary">{row.time}</td>
580
- <td className="px-5 py-3.5">
581
- <span className={\`inline-flex px-2.5 py-0.5 rounded-full text-xs font-semibold \${statusColors[row.status] ?? ''}\`}>
582
- {row.status}
583
- </span>
584
- </td>
585
- </tr>
586
- ))}
587
- </tbody>
588
- </table>
575
+ <DataTable
576
+ columns={[
577
+ { key: 'id', header: 'ID', className: 'font-mono text-xs text-label-secondary' },
578
+ { key: 'user', header: 'User', render: (r) => <span className="font-medium text-label-primary">{r.user}</span> },
579
+ { key: 'action', header: 'Action', render: (r) => <span className="text-label-secondary">{r.action}</span> },
580
+ { key: 'time', header: 'Time', render: (r) => <span className="text-label-tertiary">{r.time}</span> },
581
+ { key: 'status', header: 'Status', render: (r) => <StatusBadge status={r.status} /> },
582
+ ]}
583
+ data={activity}
584
+ keyExtractor={(r) => r.id}
585
+ />
586
+ </div>
587
+ </div>
588
+ )
589
+ }
590
+ `;
591
+ }
592
+ function genUsersPage(_o) {
593
+ return `'use client'
594
+
595
+ import { useState } from 'react'
596
+ import { PageShell, Breadcrumbs, DataTable, StatusBadge, ActionButtons, Button, Input, Select } from '@lucifer91299/ui'
597
+ import { UserPlus, Search } from 'lucide-react'
598
+
599
+ type User = { id: number; name: string; email: string; role: string; status: string; joined: string }
600
+
601
+ const USERS: User[] = [
602
+ { id: 1, name: 'Rahul Sharma', email: 'rahul@example.com', role: 'Admin', status: 'active', joined: '12 Jan 2024' },
603
+ { id: 2, name: 'Priya Mehta', email: 'priya@example.com', role: 'Member', status: 'active', joined: '03 Feb 2024' },
604
+ { id: 3, name: 'Amit Patel', email: 'amit@example.com', role: 'Member', status: 'pending', joined: '19 Mar 2024' },
605
+ { id: 4, name: 'Sneha Iyer', email: 'sneha@example.com', role: 'Editor', status: 'active', joined: '07 Apr 2024' },
606
+ { id: 5, name: 'Vikram Singh', email: 'vikram@example.com', role: 'Member', status: 'inactive', joined: '22 May 2024' },
607
+ ]
608
+
609
+ const STATUS_OPTIONS = [
610
+ { value: '', label: 'All Statuses' },
611
+ { value: 'active', label: 'Active' },
612
+ { value: 'pending', label: 'Pending' },
613
+ { value: 'inactive', label: 'Inactive' },
614
+ ]
615
+
616
+ const ROLE_OPTIONS = [
617
+ { value: '', label: 'All Roles' },
618
+ { value: 'Admin', label: 'Admin' },
619
+ { value: 'Member', label: 'Member' },
620
+ { value: 'Editor', label: 'Editor' },
621
+ ]
622
+
623
+ export default function UsersPage() {
624
+ const [search, setSearch] = useState('')
625
+ const [statusFilter, setStatus] = useState('')
626
+ const [roleFilter, setRole] = useState('')
627
+
628
+ const filtered = USERS.filter((u) => {
629
+ const q = search.toLowerCase()
630
+ if (q && !u.name.toLowerCase().includes(q) && !u.email.toLowerCase().includes(q)) return false
631
+ if (statusFilter && u.status !== statusFilter) return false
632
+ if (roleFilter && u.role !== roleFilter) return false
633
+ return true
634
+ })
635
+
636
+ return (
637
+ <div className="p-6">
638
+ <PageShell
639
+ title="Users"
640
+ subtitle={\`\${filtered.length} of \${USERS.length} users\`}
641
+ breadcrumbs={<Breadcrumbs items={[{ label: 'Users' }]} />}
642
+ actions={
643
+ <Button variant="primary">
644
+ <UserPlus className="w-4 h-4 mr-1.5" />
645
+ Add User
646
+ </Button>
647
+ }
648
+ controls={
649
+ <>
650
+ <div className="flex-1 min-w-[200px]">
651
+ <Input
652
+ placeholder="Search by name or email\u2026"
653
+ value={search}
654
+ onChange={(e) => setSearch(e.target.value)}
655
+ suffix={<Search className="w-4 h-4" />}
656
+ />
657
+ </div>
658
+ <div className="w-40">
659
+ <Select options={STATUS_OPTIONS} value={statusFilter} onChange={setStatus} placeholder="Status" />
660
+ </div>
661
+ <div className="w-36">
662
+ <Select options={ROLE_OPTIONS} value={roleFilter} onChange={setRole} placeholder="Role" />
663
+ </div>
664
+ </>
665
+ }
666
+ />
667
+
668
+ <div className="bg-white rounded-xl border border-separator shadow-sm overflow-hidden">
669
+ <DataTable
670
+ columns={[
671
+ { key: 'id', header: '#', render: (_, i) => <span className="text-label-tertiary text-footnote">{i + 1}</span>, className: 'w-10' },
672
+ {
673
+ key: 'name', header: 'Name',
674
+ render: (u: User) => (
675
+ <div>
676
+ <p className="font-medium text-label-primary">{u.name}</p>
677
+ <p className="text-footnote text-label-tertiary">{u.email}</p>
678
+ </div>
679
+ ),
680
+ },
681
+ { key: 'role', header: 'Role' },
682
+ { key: 'joined', header: 'Joined' },
683
+ { key: 'status', header: 'Status', render: (u: User) => <StatusBadge status={u.status} /> },
684
+ {
685
+ key: 'actions', header: '', className: 'w-24',
686
+ render: () => (
687
+ <ActionButtons
688
+ showView showEdit showDelete
689
+ onView={() => {}} onEdit={() => {}} onDelete={() => {}}
690
+ />
691
+ ),
692
+ },
693
+ ]}
694
+ data={filtered}
695
+ keyExtractor={(u) => u.id}
696
+ emptyMessage="No users match your filters."
697
+ />
589
698
  </div>
590
699
  </div>
591
700
  )
592
701
  }
593
702
  `;
594
703
  }
704
+ function genSettingsPage(o) {
705
+ return `'use client'
706
+
707
+ import { useState } from 'react'
708
+ import { PageShell, Breadcrumbs, Card, Input, Select, Button, AlertBanner } from '@lucifer91299/ui'
709
+
710
+ const SIDEBAR_OPTIONS = [
711
+ { value: 'full', label: 'Full \u2014 wide sidebar with labels' },
712
+ { value: 'rail', label: 'Rail \u2014 icon-only narrow sidebar' },
713
+ { value: 'both', label: 'Both \u2014 full on desktop, rail on mobile' },
714
+ ]
715
+
716
+ export default function SettingsPage() {
717
+ const [saved, setSaved] = useState(false)
718
+ const [projectName, setProject] = useState('${o.projectName}')
719
+ const [sidebar, setSidebar] = useState('${o.sidebarStyle}')
720
+ const [apiUrl, setApiUrl] = useState('${o.apiUrl}')
721
+
722
+ function handleSave() {
723
+ setSaved(true)
724
+ setTimeout(() => setSaved(false), 3000)
725
+ }
726
+
727
+ return (
728
+ <div className="p-6 space-y-6">
729
+ <PageShell
730
+ title="Settings"
731
+ subtitle="Configure your portal appearance and connection settings."
732
+ breadcrumbs={<Breadcrumbs items={[{ label: 'Settings' }]} />}
733
+ actions={<Button variant="primary" onClick={handleSave}>Save changes</Button>}
734
+ />
735
+
736
+ {saved && <AlertBanner variant="success" message="Settings saved successfully." />}
737
+
738
+ <Card className="p-6">
739
+ <h3 className="text-callout font-semibold text-label-primary mb-4">Appearance</h3>
740
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
741
+ <Input
742
+ label="Project name"
743
+ value={projectName}
744
+ onChange={(e) => setProject(e.target.value)}
745
+ />
746
+ <Select
747
+ label="Sidebar style"
748
+ options={SIDEBAR_OPTIONS}
749
+ value={sidebar}
750
+ onChange={setSidebar}
751
+ />
752
+ </div>
753
+ </Card>
754
+
755
+ <Card className="p-6">
756
+ <h3 className="text-callout font-semibold text-label-primary mb-4">Connection</h3>
757
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
758
+ <Input
759
+ label="Backend API URL"
760
+ value={apiUrl}
761
+ onChange={(e) => setApiUrl(e.target.value)}
762
+ helperText="Used by the API client for all requests."
763
+ />
764
+ <Input
765
+ label="JWT cookie name"
766
+ defaultValue="${o.jwtCookieName}"
767
+ helperText="Must match the cookie set by your login route."
768
+ />
769
+ </div>
770
+ </Card>
771
+ </div>
772
+ )
773
+ }
774
+ `;
775
+ }
595
776
 
596
777
  // src/cli/index.ts
597
778
  function write(filePath, content) {
598
- const dir = filePath.substring(0, filePath.lastIndexOf("/"));
779
+ const dir = (0, import_path.dirname)(filePath);
599
780
  if (dir) (0, import_fs.mkdirSync)(dir, { recursive: true });
600
781
  (0, import_fs.writeFileSync)(filePath, content, "utf-8");
601
782
  }
@@ -775,6 +956,8 @@ export default config
775
956
  write(f("src/app/login/page.tsx"), genLoginPage(opts));
776
957
  write(f("src/app/dashboard/layout.tsx"), genDashboardLayout(opts));
777
958
  write(f("src/app/dashboard/page.tsx"), genDashboardHomePage(opts));
959
+ write(f("src/app/dashboard/users/page.tsx"), genUsersPage(opts));
960
+ write(f("src/app/dashboard/settings/page.tsx"), genSettingsPage(opts));
778
961
  write(f("src/app/api/auth/login/route.ts"), genLoginRoute(opts));
779
962
  write(f("src/app/api/auth/user/route.ts"), genUserRoute(opts));
780
963
  write(f("src/app/api/auth/session/route.ts"), genSessionRoute(opts));
@@ -827,7 +1010,7 @@ function countFiles(dir) {
827
1010
  async function main() {
828
1011
  const { flags, positional } = parseArgs(process.argv.slice(2));
829
1012
  if (flags["help"] || flags["h"]) {
830
- log("Usage: create-portal-app [project-name] [options]");
1013
+ log("Usage: npx @lucifer91299/create-portal-app [project-name] [options]");
831
1014
  log("");
832
1015
  log(" Without options \u2192 interactive prompts for all settings");
833
1016
  log("");
@@ -843,11 +1026,11 @@ async function main() {
843
1026
  log(" --local-ui=PATH Use local @lucifer91299/ui (file: reference, for development)");
844
1027
  log("");
845
1028
  log("Examples:");
846
- log(" create-portal-app");
847
- log(" create-portal-app my-portal");
848
- log(" create-portal-app my-portal --yes");
849
- log(" create-portal-app my-portal --yes --auth=laravel --primary=#E11D48");
850
- log(" create-portal-app my-portal --yes --local-ui=../../ui");
1029
+ log(" npx @lucifer91299/create-portal-app");
1030
+ log(" npx @lucifer91299/create-portal-app my-portal");
1031
+ log(" npx @lucifer91299/create-portal-app my-portal --yes");
1032
+ log(" npx @lucifer91299/create-portal-app my-portal --yes --auth=laravel --primary=#E11D48");
1033
+ log(" npx @lucifer91299/create-portal-app my-portal --yes --local-ui=../../ui");
851
1034
  process.exit(0);
852
1035
  }
853
1036
  const projectNameArg = positional[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucifer91299/create-portal-app",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Scaffold a Next.js authenticated portal with full design system in one command",
5
5
  "license": "MIT",
6
6
  "author": "Aakash Kanojiya",