Skip to content

Commit 2e3b643

Browse files
.
0 parents  commit 2e3b643

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5355
-0
lines changed

.env.example

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This is an example of your .env file format, which pnpm db:setup will create.
2+
# Note: this must be .env, not .env.local, without further configuration changes.
3+
POSTGRES_URL=postgresql://***
4+
STRIPE_SECRET_KEY=sk_test_***
5+
STRIPE_WEBHOOK_SECRET=whsec_***
6+
BASE_URL=http://localhost:3000
7+
AUTH_SECRET=***

.gitignore

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# core
2+
.DS_Store
3+
.env
4+
.env*.local
5+
output
6+
dist
7+
target
8+
.idea
9+
.cache
10+
.output
11+
node_modules
12+
package-lock.json
13+
yarn.lock
14+
pnpm-lock.yaml
15+
.vercel
16+
private
17+
private.*
18+
private-*
19+
p-*
20+
past.*
21+
past-*
22+
*.bike
23+
*.db
24+
.repo_ignore
25+
x/
26+
27+
# dependencies
28+
/node_modules
29+
/.pnp
30+
.pnp.js
31+
.yarn/install-state.gz
32+
33+
# testing
34+
/coverage
35+
36+
# next.js
37+
/.next/
38+
/out/
39+
40+
# production
41+
/build
42+
43+
# misc
44+
.DS_Store
45+
*.pem
46+
47+
# debug
48+
npm-debug.log*
49+
yarn-debug.log*
50+
yarn-error.log*
51+
52+
# local env files
53+
.env
54+
55+
# vercel
56+
.vercel
57+
58+
# typescript
59+
*.tsbuildinfo
60+
next-env.d.ts
61+
.vscode
62+
63+
# Docker
64+
postgres_data/
65+
.env*.local
+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
2+
import {
3+
Settings,
4+
LogOut,
5+
UserPlus,
6+
Lock,
7+
UserCog,
8+
AlertCircle,
9+
UserMinus,
10+
Mail,
11+
CheckCircle,
12+
type LucideIcon,
13+
} from "lucide-react"
14+
import { ActivityType } from "@/lib/db/schema"
15+
import { getActivityLogs } from "@/lib/db/queries"
16+
17+
const iconMap: Record<ActivityType, LucideIcon> = {
18+
[ActivityType.SIGN_UP]: UserPlus,
19+
[ActivityType.SIGN_IN]: UserCog,
20+
[ActivityType.SIGN_OUT]: LogOut,
21+
[ActivityType.UPDATE_PASSWORD]: Lock,
22+
[ActivityType.DELETE_ACCOUNT]: UserMinus,
23+
[ActivityType.UPDATE_ACCOUNT]: Settings,
24+
[ActivityType.CREATE_TEAM]: UserPlus,
25+
[ActivityType.REMOVE_TEAM_MEMBER]: UserMinus,
26+
[ActivityType.INVITE_TEAM_MEMBER]: Mail,
27+
[ActivityType.ACCEPT_INVITATION]: CheckCircle,
28+
}
29+
30+
function getRelativeTime(date: Date) {
31+
const now = new Date()
32+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
33+
34+
if (diffInSeconds < 60) return "just now"
35+
if (diffInSeconds < 3600)
36+
return `${Math.floor(diffInSeconds / 60)} minutes ago`
37+
if (diffInSeconds < 86400)
38+
return `${Math.floor(diffInSeconds / 3600)} hours ago`
39+
if (diffInSeconds < 604800)
40+
return `${Math.floor(diffInSeconds / 86400)} days ago`
41+
return date.toLocaleDateString()
42+
}
43+
44+
function formatAction(action: ActivityType): string {
45+
switch (action) {
46+
case ActivityType.SIGN_UP:
47+
return "You signed up"
48+
case ActivityType.SIGN_IN:
49+
return "You signed in"
50+
case ActivityType.SIGN_OUT:
51+
return "You signed out"
52+
case ActivityType.UPDATE_PASSWORD:
53+
return "You changed your password"
54+
case ActivityType.DELETE_ACCOUNT:
55+
return "You deleted your account"
56+
case ActivityType.UPDATE_ACCOUNT:
57+
return "You updated your account"
58+
case ActivityType.CREATE_TEAM:
59+
return "You created a new team"
60+
case ActivityType.REMOVE_TEAM_MEMBER:
61+
return "You removed a team member"
62+
case ActivityType.INVITE_TEAM_MEMBER:
63+
return "You invited a team member"
64+
case ActivityType.ACCEPT_INVITATION:
65+
return "You accepted an invitation"
66+
default:
67+
return "Unknown action occurred"
68+
}
69+
}
70+
71+
export default async function ActivityPage() {
72+
const logs = await getActivityLogs()
73+
74+
return (
75+
<section className="flex-1 p-4 lg:p-8">
76+
<h1 className="text-lg lg:text-2xl font-medium text-gray-900 mb-6">
77+
Activity Log
78+
</h1>
79+
<Card>
80+
<CardHeader>
81+
<CardTitle>Recent Activity</CardTitle>
82+
</CardHeader>
83+
<CardContent>
84+
{logs.length > 0 ? (
85+
<ul className="space-y-4">
86+
{logs.map((log) => {
87+
const Icon = iconMap[log.action as ActivityType] || Settings
88+
const formattedAction = formatAction(log.action as ActivityType)
89+
90+
return (
91+
<li key={log.id} className="flex items-center space-x-4">
92+
<div className="bg-orange-100 rounded-full p-2">
93+
<Icon className="w-5 h-5 text-orange-600" />
94+
</div>
95+
<div className="flex-1">
96+
<p className="text-sm font-medium text-gray-900">
97+
{formattedAction}
98+
{log.ipAddress && ` from IP ${log.ipAddress}`}
99+
</p>
100+
<p className="text-xs text-gray-500">
101+
{getRelativeTime(new Date(log.timestamp))}
102+
</p>
103+
</div>
104+
</li>
105+
)
106+
})}
107+
</ul>
108+
) : (
109+
<div className="flex flex-col items-center justify-center text-center py-12">
110+
<AlertCircle className="h-12 w-12 text-orange-500 mb-4" />
111+
<h3 className="text-lg font-semibold text-gray-900 mb-2">
112+
No activity yet
113+
</h3>
114+
<p className="text-sm text-gray-500 max-w-sm">
115+
When you perform actions like signing in or updating your
116+
account, they'll appear here.
117+
</p>
118+
</div>
119+
)}
120+
</CardContent>
121+
</Card>
122+
</section>
123+
)
124+
}
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"use client"
2+
3+
import { startTransition, use, useActionState } from "react"
4+
import { Button } from "@/components/ui/button"
5+
import { Input } from "@/components/ui/input"
6+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
7+
import { Label } from "@/components/ui/label"
8+
import { Loader2 } from "lucide-react"
9+
import { useUser } from "@/lib/auth"
10+
import { updateAccount } from "@/app/(login)/actions"
11+
12+
type ActionState = {
13+
error?: string
14+
success?: string
15+
}
16+
17+
export default function GeneralPage() {
18+
const { userPromise } = useUser()
19+
const user = use(userPromise)
20+
const [state, formAction, isPending] = useActionState<ActionState, FormData>(
21+
updateAccount,
22+
{ error: "", success: "" },
23+
)
24+
25+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
26+
event.preventDefault()
27+
// If you call the Server Action directly, it will automatically
28+
// reset the form. We don't want that here, because we want to keep the
29+
// client-side values in the inputs. So instead, we use an event handler
30+
// which calls the action. You must wrap direct calls with startTransition.
31+
// When you use the `action` prop it automatically handles that for you.
32+
// Another option here is to persist the values to local storage. I might
33+
// explore alternative options.
34+
startTransition(() => {
35+
formAction(new FormData(event.currentTarget))
36+
})
37+
}
38+
39+
return (
40+
<section className="flex-1 p-4 lg:p-8">
41+
<h1 className="text-lg lg:text-2xl font-medium text-gray-900 mb-6">
42+
General Settings
43+
</h1>
44+
45+
<Card>
46+
<CardHeader>
47+
<CardTitle>Account Information</CardTitle>
48+
</CardHeader>
49+
<CardContent>
50+
<form className="space-y-4" onSubmit={handleSubmit}>
51+
<div>
52+
<Label htmlFor="name">Name</Label>
53+
<Input
54+
id="name"
55+
name="name"
56+
placeholder="Enter your name"
57+
defaultValue={user?.name || ""}
58+
required
59+
/>
60+
</div>
61+
<div>
62+
<Label htmlFor="email">Email</Label>
63+
<Input
64+
id="email"
65+
name="email"
66+
type="email"
67+
placeholder="Enter your email"
68+
defaultValue={user?.email || ""}
69+
required
70+
/>
71+
</div>
72+
{state.error && (
73+
<p className="text-red-500 text-sm">{state.error}</p>
74+
)}
75+
{state.success && (
76+
<p className="text-green-500 text-sm">{state.success}</p>
77+
)}
78+
<Button
79+
type="submit"
80+
className="bg-orange-500 hover:bg-orange-600 text-white"
81+
disabled={isPending}
82+
>
83+
{isPending ? (
84+
<>
85+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
86+
Saving...
87+
</>
88+
) : (
89+
"Save Changes"
90+
)}
91+
</Button>
92+
</form>
93+
</CardContent>
94+
</Card>
95+
</section>
96+
)
97+
}

0 commit comments

Comments
 (0)