@shipindays/shipindays 0.1.3 → 0.1.5
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.
Potentially problematic release.
This version of @shipindays/shipindays might be problematic. Click here for more details.
- package/README.md +1 -1
- package/index.js +0 -23
- package/package.json +1 -1
- package/templates/base/AGENTS.md +5 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/README.md +36 -0
- package/templates/base/eslint.config.mjs +18 -0
- package/templates/base/next.config.ts +7 -0
- package/templates/base/package-lock.json +6701 -0
- package/templates/base/package.json +26 -0
- package/templates/base/postcss.config.mjs +7 -0
- package/templates/base/public/file.svg +1 -0
- package/templates/base/public/globe.svg +1 -0
- package/templates/base/public/next.svg +1 -0
- package/templates/base/public/vercel.svg +1 -0
- package/templates/base/public/window.svg +1 -0
- package/templates/base/src/app/(auth)/login/page.tsx +114 -0
- package/templates/base/src/app/(auth)/signup/page.tsx +130 -0
- package/templates/base/src/app/dashboard/page.tsx +59 -0
- package/templates/base/src/app/favicon.ico +0 -0
- package/templates/base/src/app/globals.css +26 -0
- package/templates/base/src/app/layout.tsx +33 -0
- package/templates/base/src/app/page.tsx +46 -0
- package/templates/base/src/components/auth/logout-button.tsx +36 -0
- package/templates/base/src/components/branding/powered-by-shipindays.tsx +39 -0
- package/templates/base/src/lib/auth/index.ts +24 -0
- package/templates/base/src/middleware.ts +14 -0
- package/templates/base/tsconfig.json +34 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "base",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "16.2.1",
|
|
13
|
+
"react": "19.2.4",
|
|
14
|
+
"react-dom": "19.2.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tailwindcss/postcss": "^4",
|
|
18
|
+
"@types/node": "^20",
|
|
19
|
+
"@types/react": "^19",
|
|
20
|
+
"@types/react-dom": "^19",
|
|
21
|
+
"eslint": "^9",
|
|
22
|
+
"eslint-config-next": "16.2.1",
|
|
23
|
+
"tailwindcss": "^4",
|
|
24
|
+
"typescript": "^5"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// FILE: src/app/(auth)/login/page.tsx
|
|
4
|
+
// ROUTE: /login
|
|
5
|
+
// ROLE: login page UI — same for both Supabase and NextAuth
|
|
6
|
+
//
|
|
7
|
+
// This file never changes regardless of auth provider.
|
|
8
|
+
// It calls /api/auth/login (POST) which is implemented per block.
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useRouter } from "next/navigation";
|
|
13
|
+
import Link from "next/link";
|
|
14
|
+
|
|
15
|
+
export default function LoginPage() {
|
|
16
|
+
const [email, setEmail] = useState("");
|
|
17
|
+
const [password, setPassword] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
|
|
22
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
|
|
27
|
+
const res = await fetch("/api/auth/login", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ email, password }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
setError(data.error ?? "Invalid email or password.");
|
|
37
|
+
setLoading(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
router.push("/dashboard");
|
|
42
|
+
router.refresh();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
|
47
|
+
<div className="w-full max-w-sm space-y-6">
|
|
48
|
+
|
|
49
|
+
{/* Header */}
|
|
50
|
+
<div className="text-center">
|
|
51
|
+
<h1 className="text-2xl font-semibold tracking-tight">Welcome back</h1>
|
|
52
|
+
<p className="text-muted-foreground text-sm mt-1">Sign in to your account</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Form */}
|
|
56
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
57
|
+
<div>
|
|
58
|
+
<label className="text-sm font-medium block mb-1" htmlFor="email">
|
|
59
|
+
Email
|
|
60
|
+
</label>
|
|
61
|
+
<input
|
|
62
|
+
id="email"
|
|
63
|
+
type="email"
|
|
64
|
+
required
|
|
65
|
+
value={email}
|
|
66
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
67
|
+
placeholder="you@example.com"
|
|
68
|
+
className="w-full px-3 py-2 border rounded-md text-sm bg-background
|
|
69
|
+
focus:outline-none focus:ring-2 focus:ring-ring"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div>
|
|
74
|
+
<label className="text-sm font-medium block mb-1" htmlFor="password">
|
|
75
|
+
Password
|
|
76
|
+
</label>
|
|
77
|
+
<input
|
|
78
|
+
id="password"
|
|
79
|
+
type="password"
|
|
80
|
+
required
|
|
81
|
+
value={password}
|
|
82
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
83
|
+
placeholder="••••••••"
|
|
84
|
+
className="w-full px-3 py-2 border rounded-md text-sm bg-background
|
|
85
|
+
focus:outline-none focus:ring-2 focus:ring-ring"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{error && (
|
|
90
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
type="submit"
|
|
95
|
+
disabled={loading}
|
|
96
|
+
className="w-full py-2.5 bg-foreground text-background rounded-md
|
|
97
|
+
text-sm font-medium hover:bg-foreground/90
|
|
98
|
+
disabled:opacity-50 transition-colors"
|
|
99
|
+
>
|
|
100
|
+
{loading ? "Signing in..." : "Sign in"}
|
|
101
|
+
</button>
|
|
102
|
+
</form>
|
|
103
|
+
|
|
104
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
105
|
+
Don't have an account?{" "}
|
|
106
|
+
<Link href="/signup" className="underline underline-offset-4 hover:text-foreground">
|
|
107
|
+
Sign up
|
|
108
|
+
</Link>
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// FILE: src/app/(auth)/signup/page.tsx
|
|
4
|
+
// ROUTE: /signup
|
|
5
|
+
// ROLE: signup page UI — same for both Supabase and NextAuth
|
|
6
|
+
//
|
|
7
|
+
// Calls /api/auth/signup (POST) which is implemented per block.
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { useRouter } from "next/navigation";
|
|
12
|
+
import Link from "next/link";
|
|
13
|
+
|
|
14
|
+
export default function SignupPage() {
|
|
15
|
+
const [name, setName] = useState("");
|
|
16
|
+
const [email, setEmail] = useState("");
|
|
17
|
+
const [password, setPassword] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
|
|
22
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
|
|
27
|
+
const res = await fetch("/api/auth/signup", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ name, email, password }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
setError(data.error ?? "Something went wrong.");
|
|
37
|
+
setLoading(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
router.push("/dashboard");
|
|
42
|
+
router.refresh();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
|
47
|
+
<div className="w-full max-w-sm space-y-6">
|
|
48
|
+
|
|
49
|
+
{/* Header */}
|
|
50
|
+
<div className="text-center">
|
|
51
|
+
<h1 className="text-2xl font-semibold tracking-tight">Create an account</h1>
|
|
52
|
+
<p className="text-muted-foreground text-sm mt-1">Get started for free</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Form */}
|
|
56
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
57
|
+
<div>
|
|
58
|
+
<label className="text-sm font-medium block mb-1" htmlFor="name">
|
|
59
|
+
Name
|
|
60
|
+
</label>
|
|
61
|
+
<input
|
|
62
|
+
id="name"
|
|
63
|
+
type="text"
|
|
64
|
+
value={name}
|
|
65
|
+
onChange={(e) => setName(e.target.value)}
|
|
66
|
+
placeholder="Your name"
|
|
67
|
+
className="w-full px-3 py-2 border rounded-md text-sm bg-background
|
|
68
|
+
focus:outline-none focus:ring-2 focus:ring-ring"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div>
|
|
73
|
+
<label className="text-sm font-medium block mb-1" htmlFor="email">
|
|
74
|
+
Email
|
|
75
|
+
</label>
|
|
76
|
+
<input
|
|
77
|
+
id="email"
|
|
78
|
+
type="email"
|
|
79
|
+
required
|
|
80
|
+
value={email}
|
|
81
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
82
|
+
placeholder="you@example.com"
|
|
83
|
+
className="w-full px-3 py-2 border rounded-md text-sm bg-background
|
|
84
|
+
focus:outline-none focus:ring-2 focus:ring-ring"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div>
|
|
89
|
+
<label className="text-sm font-medium block mb-1" htmlFor="password">
|
|
90
|
+
Password
|
|
91
|
+
</label>
|
|
92
|
+
<input
|
|
93
|
+
id="password"
|
|
94
|
+
type="password"
|
|
95
|
+
required
|
|
96
|
+
minLength={8}
|
|
97
|
+
value={password}
|
|
98
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
99
|
+
placeholder="Min. 8 characters"
|
|
100
|
+
className="w-full px-3 py-2 border rounded-md text-sm bg-background
|
|
101
|
+
focus:outline-none focus:ring-2 focus:ring-ring"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{error && (
|
|
106
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<button
|
|
110
|
+
type="submit"
|
|
111
|
+
disabled={loading}
|
|
112
|
+
className="w-full py-2.5 bg-foreground text-background rounded-md
|
|
113
|
+
text-sm font-medium hover:bg-foreground/90
|
|
114
|
+
disabled:opacity-50 transition-colors"
|
|
115
|
+
>
|
|
116
|
+
{loading ? "Creating account..." : "Create account"}
|
|
117
|
+
</button>
|
|
118
|
+
</form>
|
|
119
|
+
|
|
120
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
121
|
+
Already have an account?{" "}
|
|
122
|
+
<Link href="/login" className="underline underline-offset-4 hover:text-foreground">
|
|
123
|
+
Sign in
|
|
124
|
+
</Link>
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// FILE: src/app/dashboard/page.tsx
|
|
2
|
+
// ROUTE: /dashboard
|
|
3
|
+
// ROLE: protected page — calls requireUser() which redirects to /login if not authed
|
|
4
|
+
//
|
|
5
|
+
// This file NEVER changes regardless of auth provider.
|
|
6
|
+
// requireUser() handles the redirect — different implementation per block,
|
|
7
|
+
// same behaviour for your app.
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
import { requireUser } from "@/src/lib/auth";
|
|
11
|
+
import LogoutButton from "@/src/components/auth/logout-button";
|
|
12
|
+
|
|
13
|
+
export default async function DashboardPage() {
|
|
14
|
+
// requireUser() returns user if logged in, redirects to /login if not
|
|
15
|
+
const user = await requireUser();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main className="min-h-screen bg-background">
|
|
19
|
+
|
|
20
|
+
{/* Navbar */}
|
|
21
|
+
<nav className="border-b px-6 py-4 flex items-center justify-between">
|
|
22
|
+
<span className="font-semibold tracking-tight">My SaaS</span>
|
|
23
|
+
<div className="flex items-center gap-4">
|
|
24
|
+
<span className="text-sm text-muted-foreground">{user.email}</span>
|
|
25
|
+
<LogoutButton />
|
|
26
|
+
</div>
|
|
27
|
+
</nav>
|
|
28
|
+
|
|
29
|
+
{/* Content */}
|
|
30
|
+
<div className="max-w-4xl mx-auto p-8 space-y-8">
|
|
31
|
+
<div>
|
|
32
|
+
<h1 className="text-3xl font-semibold tracking-tight">
|
|
33
|
+
Dashboard
|
|
34
|
+
</h1>
|
|
35
|
+
<p className="text-muted-foreground mt-1">
|
|
36
|
+
Welcome back{user.email ? `, ${user.email.split("@")[0]}` : ""}.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Placeholder content — replace with your app */}
|
|
41
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
42
|
+
{["Metric 1", "Metric 2", "Metric 3"].map((m) => (
|
|
43
|
+
<div key={m} className="rounded-lg border bg-card p-6">
|
|
44
|
+
<p className="text-sm text-muted-foreground">{m}</p>
|
|
45
|
+
<p className="text-2xl font-semibold mt-1">—</p>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="rounded-lg border border-dashed p-12 text-center">
|
|
51
|
+
<p className="text-muted-foreground text-sm">
|
|
52
|
+
Your app content goes here.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
</main>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--background: #0a0a0a;
|
|
18
|
+
--foreground: #ededed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background: var(--background);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "Create Next App",
|
|
17
|
+
description: "Generated by create next app",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html
|
|
27
|
+
lang="en"
|
|
28
|
+
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
|
29
|
+
>
|
|
30
|
+
<body className="min-h-full flex flex-col">{children}</body>
|
|
31
|
+
</html>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// FILE: src/app/(marketing)/page.tsx
|
|
2
|
+
// ROUTE: / (homepage)
|
|
3
|
+
// ROLE: public marketing landing page
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import PoweredByShipindays from "../components/branding/powered-by-shipindays";
|
|
8
|
+
|
|
9
|
+
export default function HomePage() {
|
|
10
|
+
return (
|
|
11
|
+
<main className="min-h-screen flex flex-col items-center justify-center bg-background px-4">
|
|
12
|
+
<div className="max-w-2xl text-center space-y-6">
|
|
13
|
+
|
|
14
|
+
<h1 className="text-5xl font-bold tracking-tight">
|
|
15
|
+
Your SaaS, ready to ship.
|
|
16
|
+
</h1>
|
|
17
|
+
|
|
18
|
+
<p className="text-xl text-muted-foreground">
|
|
19
|
+
Auth, payments, and email — wired up and working.
|
|
20
|
+
Just add your idea.
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<div className="flex gap-4 justify-center">
|
|
24
|
+
<Link
|
|
25
|
+
href="/signup"
|
|
26
|
+
className="px-6 py-3 bg-foreground text-background rounded-md
|
|
27
|
+
font-medium hover:bg-foreground/90 transition-colors"
|
|
28
|
+
>
|
|
29
|
+
Get started
|
|
30
|
+
</Link>
|
|
31
|
+
<Link
|
|
32
|
+
href="/login"
|
|
33
|
+
className="px-6 py-3 border rounded-md font-medium
|
|
34
|
+
hover:bg-muted transition-colors"
|
|
35
|
+
>
|
|
36
|
+
Sign in
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<footer className="border-t px-6 py-4 flex items-center justify-center">
|
|
42
|
+
<PoweredByShipindays />
|
|
43
|
+
</footer>
|
|
44
|
+
</main>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// FILE: src/components/auth/logout-button.tsx
|
|
4
|
+
// ROUTE: not a route — used in dashboard navbar
|
|
5
|
+
// ROLE: logout button — calls /api/auth/logout which is implemented per block
|
|
6
|
+
//
|
|
7
|
+
// This component never changes regardless of auth provider.
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { useRouter } from "next/navigation";
|
|
12
|
+
|
|
13
|
+
export default function LogoutButton() {
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
|
|
17
|
+
async function handleLogout() {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
|
|
20
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
21
|
+
|
|
22
|
+
router.push("/login");
|
|
23
|
+
router.refresh();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
onClick={handleLogout}
|
|
29
|
+
disabled={loading}
|
|
30
|
+
className="text-sm px-3 py-1.5 border rounded-md hover:bg-muted
|
|
31
|
+
transition-colors disabled:opacity-50"
|
|
32
|
+
>
|
|
33
|
+
{loading ? "Signing out..." : "Sign out"}
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
|
|
5
|
+
// FILE: src/components/powered-by-shipindays.tsx
|
|
6
|
+
// ROUTE: not a route drop this anywhere in your app
|
|
7
|
+
//
|
|
8
|
+
// PLEASE ADD THIS TO YOUR PRODUCTION APP
|
|
9
|
+
// This tiny badge helps shipindays grow and reach more developers.
|
|
10
|
+
// It takes 30 seconds to add and costs you nothing.
|
|
11
|
+
// The more people see it → more contributors → better templates for everyone.
|
|
12
|
+
//
|
|
13
|
+
// USAGE — add it to your root layout, footer, or dashboard:
|
|
14
|
+
//
|
|
15
|
+
// import PoweredByShipindays from "@/components/powered-by-shipindays";
|
|
16
|
+
//
|
|
17
|
+
// // in your layout or footer:
|
|
18
|
+
// <PoweredByShipindays />
|
|
19
|
+
|
|
20
|
+
export default function PoweredByShipindays() {
|
|
21
|
+
return (
|
|
22
|
+
<Link
|
|
23
|
+
href="https://shipindays.nikhilsai.com"
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full
|
|
27
|
+
border border-border bg-background
|
|
28
|
+
text-xs text-muted-foreground font-medium
|
|
29
|
+
hover:text-foreground hover:border-foreground/30
|
|
30
|
+
transition-all duration-200 group absolute right-2 bottom-2"
|
|
31
|
+
>
|
|
32
|
+
<span className="text-sm">⚡</span>
|
|
33
|
+
<span>Powered by</span>
|
|
34
|
+
<span className="text-foreground font-semibold group-hover:underline underline-offset-2">
|
|
35
|
+
shipindays
|
|
36
|
+
</span>
|
|
37
|
+
</Link>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// FILE: src/lib/auth/index.ts
|
|
2
|
+
// ROUTE: not a route — imported everywhere auth is needed
|
|
3
|
+
// ROLE: placeholder — REPLACED by chosen auth block (supabase or nextauth)
|
|
4
|
+
//
|
|
5
|
+
// CONTRACT — every auth block must export these exact functions:
|
|
6
|
+
//
|
|
7
|
+
// getCurrentUser() → returns logged-in user or null
|
|
8
|
+
// requireUser() → returns user or redirects to /login
|
|
9
|
+
// signOut() → signs out + redirects to /
|
|
10
|
+
//
|
|
11
|
+
// Your app always imports from "@/lib/auth" — never from the provider directly.
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export async function getCurrentUser() {
|
|
15
|
+
throw new Error("Auth provider not configured.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function requireUser() {
|
|
19
|
+
throw new Error("Auth provider not configured.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function signOut() {
|
|
23
|
+
throw new Error("Auth provider not configured.");
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// FILE: src/middleware.ts
|
|
2
|
+
// ROUTE: runs on every request before page renders
|
|
3
|
+
// ROLE: placeholder — REPLACED by chosen auth block
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
7
|
+
|
|
8
|
+
export function middleware(_req: NextRequest) {
|
|
9
|
+
return NextResponse.next();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const config = {
|
|
13
|
+
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
|
|
14
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|