Skip to content

Commit ba48c49

Browse files
committed
complete project
1 parent 1c0c1f6 commit ba48c49

39 files changed

+1872
-78
lines changed

.env

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
GOOGLE_CLIENT_ID = "931923526166-alv5ghor4itv6banbuvea1r6kbiah3m0.apps.googleusercontent.com"
22
GOOGLE_CLIENT_SECRET="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"
33
MONGO_URI ="mongodb+srv://ak26122k:[email protected]/test"
4-
SECRET="sjdghfueryksndkjfheriu"
4+
SECRET="sjdghfueryksndkjfheriu"
5+
6+
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="dwrhwb3pu"
7+
URL="http://localhost:3000/"

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ yarn-error.log*
2727

2828
# local env files
2929
.env*.local
30-
30+
.env
3131
# vercel
3232
.vercel
3333

actions/grabUsername.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use server';
2+
import {authOptions} from "@/app/api/auth/[...nextauth]/route";
3+
import {Page} from "@/models/Page";
4+
import mongoose from "mongoose";
5+
import {getServerSession} from "next-auth";
6+
7+
export default async function grabUsername(formData) {
8+
const username = formData.get('username');
9+
mongoose.connect(process.env.MONGO_URI);
10+
const existingPageDoc = await Page.findOne({uri:username});
11+
if (existingPageDoc) {
12+
return false;
13+
} else {
14+
const session = await getServerSession(authOptions);
15+
return await Page.create({
16+
uri:username,
17+
owner:session?.user?.email,
18+
buttons:{page:username}
19+
});
20+
}
21+
}

actions/pageActions.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use server';
2+
import {authOptions} from "@/app/api/auth/[...nextauth]/route";
3+
import {Page} from "@/models/Page";
4+
import {User} from "@/models/User";
5+
import mongoose from "mongoose";
6+
import {getServerSession} from "next-auth";
7+
8+
9+
10+
11+
12+
export async function savePageSettings(formData) {
13+
mongoose.connect(process.env.MONGO_URI);
14+
const session = await getServerSession(authOptions);
15+
if (formData.has('avatar')) {
16+
const avatarLink = formData.get('avatar');
17+
18+
await User.updateOne(
19+
{email: session.user?.email},
20+
{image: avatarLink},
21+
);
22+
}
23+
if (session) {
24+
const dataKeys = [
25+
'displayName','location',
26+
'bio', 'bgType', 'bgColor', 'bgImage',
27+
];
28+
29+
const dataToUpdate = {};
30+
for (const key of dataKeys) {
31+
if (formData.has(key)) {
32+
dataToUpdate[key] = formData.get(key);
33+
}
34+
}
35+
36+
await Page.updateOne(
37+
{owner:session?.user?.email},
38+
dataToUpdate,
39+
);
40+
41+
42+
43+
return true;
44+
}
45+
46+
return false;
47+
}
48+
export async function savePageButtons(formData) {
49+
mongoose.connect(process.env.MONGO_URI);
50+
const session = await getServerSession(authOptions);
51+
if (session) {
52+
const buttonsValues = {};
53+
formData.forEach((value, key) => {
54+
buttonsValues[key] = value;
55+
});
56+
const dataToUpdate = {buttons:buttonsValues};
57+
await Page.updateOne(
58+
{owner:session?.user?.email},
59+
dataToUpdate,
60+
);
61+
return true;
62+
}
63+
return false;
64+
}
65+
66+
67+
68+
export async function savePageLinks(links) {
69+
mongoose.connect(process.env.MONGO_URI);
70+
const session = await getServerSession(authOptions);
71+
if (session) {
72+
await Page.updateOne(
73+
{owner:session?.user?.email},
74+
{links},
75+
);
76+
} else {
77+
return false;
78+
}
79+
}

app/(app)/account/page.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { getServerSession } from "next-auth";
2+
import { authOptions } from "../../api/auth/[...nextauth]/route";
3+
import UsernameForm from "@/components/forms/UsernameForm";
4+
import { Page } from "@/models/Page";
5+
import mongoose from "mongoose";
6+
import PageSettingsForm from "@/components/forms/PageSettingForm";
7+
import PageButtonsForm from "@/components/forms/PageButtonForm";
8+
import PageLinksForm from "@/components/forms/PageLinksForm";
9+
10+
11+
export default async function AccountPage ({searchParams}) {
12+
const session = await getServerSession(authOptions);
13+
14+
const desiredUsername = searchParams?.desiredUsername;
15+
if (!session) {
16+
return redirect('/');
17+
}
18+
mongoose.connect(process.env.MONGO_URI);
19+
const page = await Page.findOne({owner: session?.user?.email});
20+
21+
22+
if (page) {
23+
return (
24+
<>
25+
26+
<PageSettingsForm page={page} user={session.user} />
27+
<PageButtonsForm page={page} user={session.user} />
28+
<PageLinksForm page={page} user={session.user} />
29+
</>
30+
);
31+
}
32+
33+
return (
34+
<div>
35+
<UsernameForm desiredUsername={desiredUsername} />
36+
</div>
37+
);
38+
}
39+
40+
41+

app/(app)/analytics/page.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
2+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
3+
import Chart from "@/components/Chart";
4+
import SectionBox from "@/components/layout/SectionBox";
5+
import {Event} from "@/models/Event";
6+
import {Page} from "@/models/Page";
7+
8+
import {differenceInDays, formatISO9075, isToday} from "date-fns";
9+
import mongoose from "mongoose";
10+
import {getServerSession} from "next-auth";
11+
import {redirect} from "next/navigation";
12+
import { FaLink } from "react-icons/fa";
13+
14+
// import {CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis} from "recharts";
15+
16+
17+
export default async function AnalyticsPage() {
18+
mongoose.connect(process.env.MONGO_URI);
19+
const session = await getServerSession(authOptions);
20+
21+
if (!session) {
22+
return redirect('/');
23+
}
24+
const page = await Page.findOne({owner: session.user.email});
25+
await Event.create({uri:page.uri, page:page.uri, type:'view'});
26+
27+
const groupedViews = await Event.aggregate([
28+
{
29+
$match: {
30+
type: 'view',
31+
uri: page.uri,
32+
}
33+
},
34+
{
35+
$group: {
36+
_id: {
37+
$dateToString: {
38+
date: "$createdAt",
39+
format: "%Y-%m-%d"
40+
},
41+
},
42+
count: {
43+
"$count": {},
44+
}
45+
},
46+
},
47+
{
48+
$sort: {_id: 1}
49+
}
50+
]);
51+
52+
const clicks = await Event.find({
53+
page: page.uri,
54+
type: 'click',
55+
});
56+
57+
return (
58+
<div>
59+
<SectionBox>
60+
<h2 className="text-xl mb-6 text-center">Views</h2>
61+
<Chart data={groupedViews.map(o => ({
62+
'date': o._id,
63+
'views': o.count,
64+
}))} />
65+
</SectionBox>
66+
<SectionBox>
67+
<h2 className="text-xl mb-6 text-center">Clicks</h2>
68+
{page.links.map(link => (
69+
<div key={link.title} className="md:flex gap-4 items-center border-t border-gray-200 py-4">
70+
<div className="text-blue-500 pl-4">
71+
72+
<FaLink/>
73+
</div>
74+
<div className="grow">
75+
<h3>{link.title || 'no title'}</h3>
76+
<p className="text-gray-700 text-sm">{link.subtitle || 'no description'}</p>
77+
<a className="text-xs text-blue-400" target="_blank" href="link.url">{link.url}</a>
78+
</div>
79+
<div className="text-center">
80+
<div className="border rounded-md p-2 mt-1 md:mt-0">
81+
<div className="text-3xl">
82+
{
83+
clicks
84+
.filter(
85+
c => c.uri === link.url
86+
&& isToday(c.createdAt)
87+
)
88+
.length
89+
}
90+
</div>
91+
<div className="text-gray-400 text-xs uppercase font-bold">clicks today</div>
92+
</div>
93+
</div>
94+
<div className="text-center">
95+
<div className="border rounded-md p-2 mt-1 md:mt-0">
96+
<div className="text-3xl">
97+
{clicks.filter(c => c.uri === link.url).length}
98+
</div>
99+
<div className="text-gray-400 text-xs uppercase font-bold">clicks total</div>
100+
</div>
101+
</div>
102+
</div>
103+
))}
104+
</SectionBox>
105+
</div>
106+
);
107+
}

app/(app)/layout.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Next.js',
3+
description: 'Generated by Next.js',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}

app/(app)/template.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {authOptions} from "@/app/api/auth/[...nextauth]/route";
2+
3+
import {Page} from "@/models/Page";
4+
import { GiHamburgerMenu } from "react-icons/gi";
5+
import { FaLink } from "react-icons/fa";
6+
import mongoose from "mongoose";
7+
import {getServerSession} from "next-auth";
8+
import {Lato} from 'next/font/google'
9+
import '../globals.css'
10+
11+
import Image from "next/image";
12+
import Link from "next/link";
13+
import {redirect} from "next/navigation";
14+
import {Toaster} from "react-hot-toast";
15+
import AppSidebar from "../../components/layout/AppSidebar";
16+
import { IoIosArrowRoundBack, IoIosArrowRoundForward } from "react-icons/io";
17+
18+
const lato = Lato({ subsets: ['latin'], weight: ['400','700'] })
19+
20+
export const metadata = {
21+
title: 'Create Next App',
22+
description: 'Generated by create next app',
23+
}
24+
25+
26+
27+
28+
export default async function AppTemplate({ children }) {
29+
const session = await getServerSession(authOptions);
30+
if (!session) {
31+
return redirect('/');
32+
}
33+
mongoose.connect(process.env.MONGO_URI);
34+
const page = await Page.findOne({owner: session.user.email});
35+
return (
36+
<html lang="en">
37+
<body className={lato.className}>
38+
<Toaster />
39+
<main className="md:flex min-h-screen">
40+
{page && <>
41+
<label htmlFor="navCb" className="md:hidden ml-8 mt-4 p-4 rounded-md bg-white shadow inline-flex items-center gap-2 cursor-pointer">
42+
<GiHamburgerMenu />
43+
<span>Open navigation</span>
44+
</label>
45+
<input id="navCb" type="checkbox" className="hidden" />
46+
<label htmlFor="navCb" className="hidden backdrop fixed inset-0 bg-black/80 z-10"></label>
47+
<aside className="bg-white w-48 p-4 pt-6 shadow fixed md:static -left-48 top-0 bottom-0 z-20 transition-all">
48+
<div className="sticky top-0 pt-2">
49+
<div className="rounded-full overflow-hidden aspect-square w-24 mx-auto">
50+
<Image src={session.user.image} width={256} height={256} alt={'avatar'} />
51+
</div>
52+
{page && (
53+
<Link
54+
target="_blank"
55+
href={'/'+page.uri}
56+
className="text-center mt-4 flex gap-1 items-center justify-center">
57+
<span className="font-semibold text-blue-600">View Page</span>
58+
<span><IoIosArrowRoundForward className="w-6 h-6 text-blue-600" /></span>
59+
60+
61+
</Link>
62+
)}
63+
<div className="text-center">
64+
<AppSidebar />
65+
</div>
66+
</div>
67+
</aside>
68+
</>
69+
}
70+
<div className="grow">
71+
{children}
72+
</div>
73+
</main>
74+
</body>
75+
</html>
76+
)
77+
}
78+

app/(page)/[uri]/layout.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
import {Lato} from 'next/font/google'
3+
import '../../globals.css'
4+
5+
const lato = Lato({ subsets: ['latin'], weight: ['400', '700'] })
6+
7+
export const metadata = {
8+
title: 'Create Next App',
9+
description: 'Generated by create next app',
10+
}
11+
12+
export default function RootLayout({ children }) {
13+
return (
14+
<html lang="en">
15+
<body className={lato.className}>
16+
<main>
17+
{children}
18+
</main>
19+
</body>
20+
</html>
21+
)
22+
}

0 commit comments

Comments
 (0)