0% found this document useful (0 votes)
12 views147 pages

OnlyLinks PHP to NextJS Migration Plan

The document outlines a comprehensive migration plan for transitioning a PHP application to NextJS, divided into five phases: Foundation & Architecture, User Features, Admin Panel, Testing & Optimization, and Launch & Migration. Each phase includes specific tasks, such as setting up project structure, implementing user and admin features, integrating payment systems, and ensuring thorough testing before deployment. Key technical considerations are also highlighted, including authentication flow, database schema, API structure, and error handling.

Uploaded by

chotu.agrawal181
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as XLSX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
12 views147 pages

OnlyLinks PHP to NextJS Migration Plan

The document outlines a comprehensive migration plan for transitioning a PHP application to NextJS, divided into five phases: Foundation & Architecture, User Features, Admin Panel, Testing & Optimization, and Launch & Migration. Each phase includes specific tasks, such as setting up project structure, implementing user and admin features, integrating payment systems, and ensuring thorough testing before deployment. Key technical considerations are also highlighted, including authentication flow, database schema, API structure, and error handling.

Uploaded by

chotu.agrawal181
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as XLSX, PDF, TXT or read online on Scribd

# OnlyLinks: PHP to NextJS Migration Project Plan

## Phase 1: Foundation & Architecture (2 weeks)

### Week 1: Project Setup & Authentication


1. **Project Structure Setup**
- Initialize NextJS app with TypeScript
- Configure ESLint, Prettier, and Husky for code quality
- Set up folder structure following NextJS 13+ App Router conventions

2. **Database Integration**(Use Drizzle ORM instead of prisma)


- Install and configure Prisma ORM
- Create [Link] file based on existing PHP database structure
- Set up database connection and environment variables

3. **Authentication System**
- Implement [Link] with the following providers:
- Credentials provider (email/password)
- OAuth providers if needed
- Create login, register, and password reset pages
- Implement session management and protected routes

Core API Routes & Models


1. **User API Routes**
- `/api/auth/[...nextauth].js` - Authentication endpoints
- `/api/users` - User management endpoints
- `/api/users/profile` - Profile management

2. **Admin Authentication**
- Create separate admin authentication flow
- Implement admin session validation middleware
- Set up admin-only protected routes

3. **File Upload System**


- Implement file upload functionality using AWS S3 or similar
- Create API routes for file management
- Set up image optimization and processing

## Phase 2: User Features (3 weeks)

### Week 3: User Dashboard


1. **Dashboard UI**
- Create responsive dashboard layout
- Implement sidebar navigation
- Build dashboard overview components

2. **Profile Management**
- Create profile editing interface
- Implement avatar upload and management
- Add account settings functionality

3. **Link Management**
- Build link creation and editing interface
- Implement link analytics display
- Create link organization features

### Week 4: Analytics & Customization


1. **Analytics System**
- Implement link click tracking
- Create visitor analytics collection
- Build analytics dashboard with charts

2. **Theme Customization**
- Create theme selection interface
- Implement font management
- Build color scheme customization

3. **Mobile Responsiveness**
- Ensure all user interfaces work on mobile
- Implement responsive design patterns
- Test across various device sizes

### Week 5: Billing & Subscriptions


1. **Stripe Integration**
- Set up Stripe API connection
- Implement payment processing
- Create webhook handlers for payment events

2. **Subscription Management**
- Build subscription plan selection interface
- Implement subscription lifecycle management
- Create billing history and invoice display

3. **Plan Features**
- Implement feature access based on subscription level
- Create upgrade/downgrade flows
- Add trial period functionality

## Phase 3: Admin Panel (3 weeks)


### Week 6: Admin Dashboard
1. **Admin Layout**
- Create admin dashboard layout
- Implement admin navigation
- Build admin overview components

2. **User Management**
- Create user listing with search and filters
- Implement user editing and management
- Add user activity monitoring

3. **Content Management**
- Build page editor for static content
- Implement language management
- Create theme and font management interfaces

### Week 7: System Management


1. **Settings Management**
- Create system settings interface
- Implement configuration management
- Add environment variable management

2. **Plan Management**
- Build subscription plan editor
- Implement feature management for plans
- Create pricing configuration

3. **Agency Features**
- Implement agency dashboard
- Create client management interface
- Build agency-specific analytics

### Week 8: Advanced Admin Features


1. **Reporting**
- Create financial reports
- Implement user growth analytics
- Build custom report generation

2. **Bulk Operations**
- Add bulk user management
- Implement batch operations for links
- Create import/export functionality

3. **Admin API**
- Complete remaining admin API endpoints
- Implement comprehensive error handling
- Add rate limiting and security features

## Phase 4: Testing & Optimization (2 weeks)

### Week 9: Testing


1. **Unit Testing**
- Write tests for core functionality
- Implement API route testing
- Create component tests

2. **Integration Testing**
- Test end-to-end user flows
- Verify admin functionality
- Test payment processing

3. **Performance Testing**
- Analyze and optimize page load times
- Implement code splitting and lazy loading
- Optimize database queries

### Week 10: Deployment Preparation


1. **SEO Optimization**
- Implement metadata management
- Add sitemap generation
- Configure [Link]

2. **Accessibility**
- Audit and fix accessibility issues
- Implement keyboard navigation
- Add screen reader support

3. **Documentation**
- Create user documentation
- Write admin guide
- Document API endpoints

## Phase 5: Launch & Migration (2 weeks)

### Week 11: Deployment


1. **Staging Deployment**
- Deploy to staging environment
- Perform final testing
- Fix any discovered issues

2. **Data Migration**
- Create data migration scripts
- Test migration process
- Prepare rollback plan

3. **CI/CD Setup**
- Configure continuous integration
- Set up automated testing
- Implement deployment pipeline

### Week 12: Launch & Monitoring


1. **Production Deployment**
- Deploy to production environment
- Implement monitoring tools
- Set up error tracking

2. **User Migration**
- Migrate existing users
- Send communication about the new platform
- Provide support for transition

3. **Post-Launch Support**
- Monitor system performance
- Address user feedback
- Fix any critical issues

## Key Technical Considerations

### Authentication Flow


- Implement JWT-based authentication
- Create secure password reset flow
- Add two-factor authentication option

### Database Schema


- Maintain compatibility with existing data
- Implement proper relations between tables
- Use migrations for schema changes

### API Structure


```
/api
/auth
/[...nextauth].js
/admin-login
/password-reset
/users
/[id]
/profile
/links
/admin
/users
/settings
/plans
/themes
/billing
/subscriptions
/invoices
/webhooks
/analytics
/links
/visitors
/reports
```

### Scheduled Tasks


- Replace [Link] with Vercel Cron Jobs or similar
- Implement the following scheduled tasks:
- Subscription status checking
- Session cleanup
- Analytics aggregation

### Error Handling


- Implement global error boundary
- Create custom error pages
- Add comprehensive logging

This plan provides a structured approach to migrating your PHP application to NextJS
# OnlyLinks: PHP to NextJS Migration Technical Implementation Guide

This guide provides detailed technical instructions for migrating the Only

## Phase 1: Foundation & Architecture

### Week 1: Project Setup & Authentication

uter conventions #### 1.1 Project Structure Setup

```bash
# Initialize NextJS app with TypeScript
abase structure npx create-next-app@latest onlylinks-next --typescript --eslint --app --src

# Install additional dependencies


cd onlylinks-next
npm install drizzle-orm @vercel/postgres drizzle-kit
npm install next-auth bcrypt jsonwebtoken
npm install zod react-hook-form @hookform/resolvers
npm install tailwindcss postcss autoprefixer
npm install lucide-react
npm install -D prettier prettier-plugin-tailwindcss husky lint-staged

# Set up Husky
npx husky init
```

Create the following folder structure:


```
src/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ ├── signup/
│ │ └── forgot-password/
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── links/
│ │ └── settings/
│ ├── (admin)/
│ │ ├── admin/
│ │ └── admin-dashboard/
│ ├── api/
│ │ ├── auth/
│ │ ├── users/
│ │ ├── links/
│ │ └── admin/
│ └── [Link]
├── components/
│ ├── ui/
│ ├── forms/
│ ├── dashboard/
│ └── admin/
├── lib/
│ ├── db/
│ │ ├── schema/
│ │ ├── [Link]
│ │ └── migrations/
│ ├── auth/
│ └── utils/
├── [Link]
└── types/
```

#### 1.2 Database Integration with Drizzle ORM

Create the database schema files:

```javascript
// src/lib/db/[Link]
import { drizzle } from 'drizzle-orm/vercel-postgres';
import { sql } from '@vercel/postgres';

export const db = drizzle(sql);


```

```javascript
// src/lib/db/schema/[Link]
import { pgTable, serial, varchar, boolean, timestamp, text } from 'drizzle
import { relations } from 'drizzle-orm';
import { links } from './links';
import { subscriptions } from './subscriptions';

export const users = pgTable('users', {


id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
password: varchar('password', { length: 255 }).notNull(),
name: varchar('name', { length: 255 }),
username: varchar('username', { length: 100 }).unique(),
active: boolean('active').default(true),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
role: varchar('role', { length: 20 }).default('user'),
profileImage: text('profile_image'),
pageStatus: boolean('page_status').default(false),
});

export const usersRelations = relations(users, ({ many }) => ({


links: many(links),
subscriptions: many(subscriptions),
}));
```

Create similar schema files for other tables (links, subscriptions, plans, e

Set up Drizzle migrations:

```javascript
// [Link]
export default {
schema: './src/lib/db/schema',
out: './src/lib/db/migrations',
driver: 'pg',
dbCredentials: {
connectionString: [Link].DATABASE_URL,
}
};
```

```bash
# Generate migrations
npx drizzle-kit generate

# Apply migrations
npx drizzle-kit push
```

#### 1.3 Authentication System with [Link]

```javascript
// src/lib/auth/[Link]
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import { eq } from "drizzle-orm";
import bcrypt from "bcrypt";

export const authOptions = {


session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
signOut: "/",
error: "/login",
},
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}

const user = await [Link]({


where: eq([Link], [Link]),
});

if (!user || ![Link]) {
return null;
}

const passwordMatch = await [Link](


[Link],
[Link]
);

if (!passwordMatch) {
return null;
}

return {
id: [Link](),
email: [Link],
name: [Link],
role: [Link],
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
[Link] = [Link];
[Link] = [Link];
}
return token;
},
async session({ session, token }) {
if (token) {
[Link] = [Link];
[Link] = [Link];
}
return session;
},
},
};
```

```javascript
// src/app/api/auth/[...nextauth]/[Link]
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth/auth-options";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };


```

Create login page:

```jsx
// src/app/(auth)/login/[Link]
use client;

import { useState } from "react";


import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
const loginSchema = [Link]({
email: [Link]().email("Please enter a valid email"),
password: [Link]().min(6, "Password must be at least 6 characters"),
});

export default function LoginPage() {


const router = useRouter();
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);

const { register, handleSubmit, formState: { errors } } = useForm({


resolver: zodResolver(loginSchema),
});

const onSubmit = async (data) => {


setIsLoading(true);
setError("");

try {
const result = await signIn("credentials", {
email: [Link],
password: [Link],
redirect: false,
});

if (result?.error) {
setError("Invalid email or password");
setIsLoading(false);
ur PHP application to NextJS while ensuring all fu return;
}

[Link]("/dashboard");
[Link]();
} catch (error) {
setError("An error occurred. Please try again.");
setIsLoading(false);
}
};

return (
<div className="flex min-h-screen flex-col items-center justify-center
<div className="w-full max-w-md space-y-8 rounded-lg border p-6 s
<div className="text-center">
<h1 className="text-2xl font-bold">Login to OnlyLinks</h1>
<p className="mt-2 text-gray-600">Enter your credentials to acce
</div>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700"
{error}
</div>
)}

<form onSubmit={handleSubmit(onSubmit)} className="mt-8 spac


<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium"
Email
</label>
<input
id="email"
type="email"
{...register("email")}
className="mt-1 block w-full rounded-md border border-gray-
/>
{[Link] && (
<p className="mt-1 text-sm text-red-600">{[Link]
)}
</div>

<div>
<label htmlFor="password" className="block text-sm font-med
Password
</label>
<input
id="password"
type="password"
{...register("password")}
className="mt-1 block w-full rounded-md border border-gray-
/>
{[Link] && (
<p className="mt-1 text-sm text-red-600">{[Link].m
)}
</div>
</div>

<div className="flex items-center justify-between">


<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-blue-600 fo
/>
<label htmlFor="remember-me" className="ml-2 block text-sm
Remember me
</label>
</div>

<div className="text-sm">
<Link href="/forgot-password" className="font-medium text-bl
Forgot your password?
</Link>
</div>
</div>

<div>
<button
type="submit"
disabled={isLoading}
className="flex w-full justify-center rounded-md bg-blue-600 p
>
{isLoading ? "Logging in..." : "Sign in"}
</button>
</div>

<div className="text-center text-sm">


Don't have an account?{" "}
<Link href="/signup" className="font-medium text-blue-600 hov
Sign up
</Link>
</div>
</form>
</div>
</div>
);
}
```

Create signup page with similar structure.

### Week 2: Core API Routes & Models

#### 2.1 User API Routes

```javascript
// src/app/api/users/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import bcrypt from "bcrypt";
import * as z from "zod";

const userSchema = [Link]({


name: [Link]().min(2, "Name must be at least 2 characters"),
email: [Link]().email("Please enter a valid email"),
password: [Link]().min(6, "Password must be at least 6 characters"),
username: [Link]().min(3, "Username must be at least 3 characters"),
});

export async function POST(request) {


try {
const body = await [Link]();

const validation = [Link](body);


if (![Link]) {
return [Link](
{ error: [Link][0].message },
{ status: 400 }
);
}

const { name, email, password, username } = body;

// Check if email already exists


const existingUserByEmail = await [Link]({
where: eq([Link], email),
});

if (existingUserByEmail) {
return [Link](
{ error: "Email already in use" },
{ status: 400 }
);
}

// Check if username already exists


const existingUserByUsername = await [Link]({
where: eq([Link], username),
});

if (existingUserByUsername) {
return [Link](
{ error: "Username already taken" },
{ status: 400 }
);
}

// Hash password
const hashedPassword = await [Link](password, 10);

// Create user
const newUser = await [Link](users).values({
name,
email,
password: hashedPassword,
username,
active: true,
role: "user",
createdAt: new Date(),
updatedAt: new Date(),
}).returning();

const user = newUser[0];

return [Link]({
id: [Link],
name: [Link],
email: [Link],
username: [Link],
}, { status: 201 });
} catch (error) {
[Link]("Error creating user:", error);
return [Link](
{ error: "Failed to create user" },
{ status: 500 }
);
}
}
```

```javascript
// src/app/api/users/[id]/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import { eq } from "drizzle-orm";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
export async function GET(request, { params }) {
try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Only allow users to access their own data or admins to access any d
if ([Link] !== [Link] && [Link] !== "admin") {
return [Link](
{ error: "Forbidden" },
{ status: 403 }
);
}

const user = await [Link]({


where: eq([Link], parseInt([Link])),
});

if (!user) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Don't return password


const { password, ...userWithoutPassword } = user;

return [Link](userWithoutPassword);
} catch (error) {
[Link]("Error fetching user:", error);
return [Link](
{ error: "Failed to fetch user" },
{ status: 500 }
);
}
}

export async function PATCH(request, { params }) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Only allow users to update their own data or admins to update any
if ([Link] !== [Link] && [Link] !== "admin") {
return [Link](
{ error: "Forbidden" },
{ status: 403 }
);
}

const body = await [Link]();

// Update user
const updatedUser = await [Link](users)
.set({
...body,
updatedAt: new Date(),
})
.where(eq([Link], parseInt([Link])))
.returning();

if (![Link]) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Don't return password


const { password, ...userWithoutPassword } = updatedUser[0];

return [Link](userWithoutPassword);
} catch (error) {
[Link]("Error updating user:", error);
return [Link](
{ error: "Failed to update user" },
{ status: 500 }
);
}
}
```

#### 2.2 Admin Authentication

```javascript
// src/[Link]
import { NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export async function middleware(request) {


const token = await getToken({ req: request });

// Check if user is authenticated


if (!token) {
return [Link](new URL("/login", [Link]));
}

// Admin routes protection


if ([Link]("/admin") && [Link] !== "
return [Link](new URL("/dashboard", [Link]));
}

return [Link]();
}

export const config = {


matcher: [
"/dashboard/:path*",
"/admin/:path*",
"/api/admin/:path*",
],
};
```

```javascript
// src/app/api/admin/login/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import { eq, and } from "drizzle-orm";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";

export async function POST(request) {


try {
const body = await [Link]();
const { username, password } = body;

if (!username || !password) {
return [Link](
{ error: "Username and password are required" },
{ status: 400 }
);
}

// Find admin user


const user = await [Link]({
where: and(
eq([Link], username),
eq([Link], "admin")
),
});

if (!user || ![Link]) {
return [Link](
{ error: "Invalid credentials" },
{ status: 401 }
);
}

// Verify password
const passwordMatch = await [Link](password, [Link]

if (!passwordMatch) {
return [Link](
{ error: "Invalid credentials" },
{ status: 401 }
);
}

// Create admin token


const adminToken = [Link](
{
id: [Link],
role: [Link],
},
[Link].ADMIN_JWT_SECRET,
{ expiresIn: "1d" }
);

// Set cookie with admin token


const response = [Link]({ success: true });
[Link]("admin_token", adminToken, {
httpOnly: true,
secure: [Link].NODE_ENV === "production",
sameSite: "strict",
maxAge: 86400, // 1 day
path: "/",
});

return response;
} catch (error) {
[Link]("Admin login error:", error);
return [Link](
{ error: "Authentication failed" },
{ status: 500 }
);
}
}
```

#### 2.3 File Upload System

```javascript
// src/app/api/upload/[Link]
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { writeFile } from "fs/promises";
import { join } from "path";
import { v4 as uuidv4 } from "uuid";

export async function POST(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const formData = await [Link]();


const file = [Link]("file");

if (!file) {
return [Link](
{ error: "No file uploaded" },
{ status: 400 }
);
}

// Validate file type


const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (![Link]([Link])) {
return [Link](
{ error: "File type not allowed" },
{ status: 400 }
);
}

// Validate file size (max 5MB)


if ([Link] > 5 * 1024 * 1024) {
return [Link](
{ error: "File too large (max 5MB)" },
{ status: 400 }
);
}

const bytes = await [Link]();


const buffer = [Link](bytes);

// Generate unique filename


const fileName = `${uuidv4()}-${[Link](/\s/g, "-")}`;
const uploadDir = join([Link](), "public", "uploads");
const filePath = join(uploadDir, fileName);

// Save file
await writeFile(filePath, buffer);

// Return file URL


const fileUrl = `/uploads/${fileName}`;

return [Link]({ url: fileUrl });


} catch (error) {
[Link]("File upload error:", error);
return [Link](
{ error: "Failed to upload file" },
{ status: 500 }
);
}
}
```

## Phase 2: User Features

### Week 3: User Dashboard

#### 3.1 Dashboard UI

Create a dashboard layout:

```jsx
// src/app/(dashboard)/[Link]
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { redirect } from "next/navigation";
import Sidebar from "@/components/dashboard/Sidebar";
import Header from "@/components/dashboard/Header";

export default async function DashboardLayout({ children }) {


const session = await getServerSession(authOptions);

if (!session) {
redirect("/login");
}

return (
<div className="flex h-screen bg-gray-50">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<Header user={[Link]} />
<main className="flex-1 overflow-y-auto p-4 md:p-6">{children}</
</div>
</div>
);
}
```

Create the dashboard components:

```jsx
// src/components/dashboard/[Link]
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
Link as LinkIcon,
Settings,
BarChart,
LogOut
} from "lucide-react";
import { signOut } from "next-auth/react";

export default function Sidebar() {


const pathname = usePathname();

const navItems = [
{
name: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
name: "Links",
href: "/links",
icon: LinkIcon,
},
{
name: "Analytics",
href: "/analytics",
icon: BarChart,
},
{
name: "Settings",
href: "/settings",
icon: Settings,
},
];

return (
<aside className="hidden w-64 flex-shrink-0 bg-white shadow-md m
<div className="flex h-16 items-center justify-center border-b">
<Link href="/dashboard" className="text-xl font-bold">OnlyLinks<
</div>
<nav className="flex flex-1 flex-col p-4">
<ul className="space-y-1">
{[Link]((item) => {
const isActive = pathname === [Link];
return (
<li key={[Link]}>
<Link
href={[Link]}
className={`flex items-center rounded-md px-4 py-2 text-sm
isActive
? "bg-blue-50 text-blue-700"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<[Link] className="mr-3 h-5 w-5" />
{[Link]}
</Link>
</li>
);
})}
</ul>
<div className="mt-auto">
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="flex w-full items-center rounded-md px-4 py-2 text-s
>
<LogOut className="mr-3 h-5 w-5" />
Sign out
</button>
</div>
</nav>
</aside>
);
}
```

```jsx
// src/components/dashboard/[Link]
import { useState } from "react";
import { Menu, Bell, User } from "lucide-react";
import Link from "next/link";
import Image from "next/image";

export default function Header({ user }) {


const [isProfileOpen, setIsProfileOpen] = useState(false);

return (
<header className="flex h-16 items-center justify-between border-b
<button className="rounded-md p-2 text-gray-500 hover:bg-gray-10
<Menu className="h-6 w-6" />
</button>

<div className="ml-auto flex items-center space-x-4">


<button className="rounded-md p-2 text-gray-500 hover:bg-gray-1
<Bell className="h-5 w-5" />
</button>

<div className="relative">
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center space-x-2 rounded-md p-2 text-gra
>
<div className="relative h-8 w-8 overflow-hidden rounded-full b
{[Link] ? (
<Image
src={[Link]}
alt={[Link] || "User"}
fill
className="object-cover"
/>
):(
<User className="h-full w-full p-1" />
)}
</div>
<span className="hidden text-sm font-medium md:block">
{[Link] || [Link]}
</span>
</button>

{isProfileOpen && (
<div className="absolute right-0 mt-2 w-48 rounded-md bg-whit
<Link
href="/settings/profile"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gra
onClick={() => setIsProfileOpen(false)}
>
Your Profile
</Link>
<Link
href="/settings"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gra
onClick={() => setIsProfileOpen(false)}
>
Settings
</Link>
<button
onClick={() => {
setIsProfileOpen(false);
signOut({ callbackUrl: "/" });
}}
className="block w-full px-4 py-2 text-left text-sm text-gray-70
>
Sign out
</button>
</div>
)}
</div>
</div>
</header>
);
}
```

#### 3.2 Link Management

Create the links schema:

```javascript
// src/lib/db/schema/[Link]
import { pgTable, serial, varchar, text, timestamp, integer, boolean } from
import { relations } from 'drizzle-orm';
import { users } from './users';

export const links = pgTable('links', {


id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => [Link]),
title: varchar('title', { length: 255 }).notNull(),
url: text('url').notNull(),
active: boolean('active').default(true),
clicks: integer('clicks').default(0),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
position: integer('position').default(0),
customIcon: text('custom_icon'),
});

export const linksRelations = relations(links, ({ one }) => ({


user: one(users, {
fields: [[Link]],
references: [[Link]],
}),
}));
```

Create the links API:


// src/app/api/links/[Link] (continued)
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { links } from "@/lib/db/schema/links";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import * as z from "zod";

const linkSchema = [Link]({


title: [Link]().min(1, "Title is required"),
url: [Link]().url("Please enter a valid URL"),
active: [Link]().optional(),
customIcon: [Link]().optional(),
});

export async function GET(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const userLinks = await [Link]({


where: eq([Link], parseInt([Link])),
orderBy: [asc([Link])],
});

return [Link](userLinks);
} catch (error) {
[Link]("Error fetching links:", error);
return [Link](
{ error: "Failed to fetch links" },
{ status: 500 }
);
}
}

export async function POST(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await [Link]();

const validation = [Link](body);


if (![Link]) {
return [Link](
{ error: [Link][0].message },
{ status: 400 }
);
}

// Get highest position for user's links


const highestPositionLink = await [Link]({
where: eq([Link], parseInt([Link])),
orderBy: [desc([Link])],
});

const newPosition = highestPositionLink ? [Link]

// Create link
const newLink = await [Link](links).values({
userId: parseInt([Link]),
title: [Link],
url: [Link],
active: [Link] ?? true,
customIcon: [Link],
position: newPosition,
createdAt: new Date(),
updatedAt: new Date(),
}).returning();

return [Link](newLink[0], { status: 201 });


} catch (error) {
[Link]("Error creating link:", error);
return [Link](
{ error: "Failed to create link" },
{ status: 500 }
);
}
}

Create the links page:


// src/app/(dashboard)/links/[Link]
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Plus, Edit, Trash2, ExternalLink, Move } from "lucide-react";
import { DragDropContext, Droppable, Draggable } from "react-beautifu
export default function LinksPage() {
const router = useRouter();
const [links, setLinks] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchLinks = async () => {
try {
const response = await fetch("/api/links");

if (![Link]) {
throw new Error("Failed to fetch links");
}

const data = await [Link]();


setLinks(data);
} catch (error) {
setError([Link]);
} finally {
setIsLoading(false);
}
};

fetchLinks();
}, []);

const handleDragEnd = async (result) => {


if (![Link]) return;

const items = [Link](links);


const [reorderedItem] = [Link]([Link], 1);
[Link]([Link], 0, reorderedItem);

// Update positions
const updatedItems = [Link]((item, index) => ({
...item,
position: index,
}));

setLinks(updatedItems);
// Update positions in database
try {
const movedItem = updatedItems[[Link]];
await fetch(`/api/links/${[Link]}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: [Link]({ position: [Link] }),
});
} catch (error) {
[Link]("Error updating position:", error);
}
};

const handleDeleteLink = async (id) => {


if (!confirm("Are you sure you want to delete this link?")) return;

try {
const response = await fetch(`/api/links/${id}`, {
method: "DELETE",
});

if (![Link]) {
throw new Error("Failed to delete link");
}

setLinks([Link]((link) => [Link] !== id));


} catch (error) {
[Link]("Error deleting link:", error);
}
};

const handleToggleActive = async (id, active) => {


try {
const response = await fetch(`/api/links/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: [Link]({ active: !active }),
});

if (![Link]) {
throw new Error("Failed to update link");
}

setLinks(
[Link]((link) =>
[Link] === id ? { ...link, active: !active } : link
)
);
} catch (error) {
[Link]("Error updating link:", error);
}
};

if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 bord
<p className="mt-2 text-gray-600">Loading links...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<p className="text-red-600">{error}</p>
<button
onClick={() => [Link]()}
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white h
>
Try again
</button>
</div>
</div>
);
}

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Your Links</h1>
<button
onClick={() => [Link]("/links/new")}
className="flex items-center rounded-md bg-blue-600 px-4 py-2 t
>
<Plus className="mr-2 h-4 w-4" />
Add Link
</button>
</div>

{[Link] === 0 ? (
<div className="rounded-md bg-gray-50 p-8 text-center">
<p className="text-gray-600">You don't have any links yet.</p>
<button
onClick={() => [Link]("/links/new")}
className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white h
>
Create your first link
</button>
</div>
):(
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="links">
{(provided) => (
<div
{...[Link]}
ref={[Link]}
className="space-y-4"
>
{[Link]((link, index) => (
<Draggable
key={[Link]}
draggableId={[Link]()}
index={index}
>
{(provided) => (
<div
ref={[Link]}
{...[Link]}
className={`rounded-md border p-4 ${
[Link] ? "bg-white" : "bg-gray-50"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div
{...[Link]}
className="mr-3 cursor-move text-gray-400 hover:text
>
<Move className="h-5 w-5" />
</div>
<div>
<h3 className="font-medium">{[Link]}</h3>
<a
href={[Link]}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-sm text-gray-500 ho
>
{[Link]}
<ExternalLink className="ml-1 h-3 w-3" />
</a>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center">
<span className="mr-2 text-sm text-gray-500">
{[Link]} clicks
</span>
<label className="relative inline-flex cursor-pointer ite
<input
type="checkbox"
checked={[Link]}
onChange={() =>
handleToggleActive([Link], [Link])
}
className="peer sr-only"
/>
<div className="peer h-6 w-11 rounded-full bg-gray-2
</label>
</div>
<button
onClick={() => [Link](`/links/${[Link]}`)}
className="rounded-md p-2 text-gray-500 hover:bg-gr
>
<Edit className="h-5 w-5" />
</button>
<button
onClick={() => handleDeleteLink([Link])}
className="rounded-md p-2 text-gray-500 hover:bg-gr
>
<Trash2 className="h-5 w-5" />
</button>
</div>
</div>
</div>
)}
</Draggable>
))}
{[Link]}
</div>
)}
</Droppable>
</DragDropContext>
)}
</div>
);
}

Week 4: Analytics & Customization


4.1 Analytics System
Create the analytics schema:
// src/lib/db/schema/[Link]
import { pgTable, serial, integer, timestamp, varchar, text } from 'drizzle-
import { relations } from 'drizzle-orm';
import { links } from './links';
import { users } from './users';

export const linkClicks = pgTable('link_clicks', {


id: serial('id').primaryKey(),
linkId: integer('link_id').notNull().references(() => [Link]),
userId: integer('user_id').notNull().references(() => [Link]),
clickedAt: timestamp('clicked_at').defaultNow(),
ipAddress: varchar('ip_address', { length: 45 }),
userAgent: text('user_agent'),
referrer: text('referrer'),
country: varchar('country', { length: 2 }),
city: varchar('city', { length: 100 }),
});

export const linkClicksRelations = relations(linkClicks, ({ one }) => ({


link: one(links, {
fields: [[Link]],
references: [[Link]],
}),
user: one(users, {
fields: [[Link]],
references: [[Link]],
}),
}));

export const pageViews = pgTable('page_views', {


id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => [Link]),
viewedAt: timestamp('viewed_at').defaultNow(),
ipAddress: varchar('ip_address', { length: 45 }),
userAgent: text('user_agent'),
referrer: text('referrer'),
country: varchar('country', { length: 2 }),
city: varchar('city', { length: 100 }),
});

export const pageViewsRelations = relations(pageViews, ({ one }) => ({


user: one(users, {
fields: [[Link]],
references: [[Link]],
}),
}));

Create the analytics tracking API:


// src/app/api/analytics/track-click/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { linkClicks } from "@/lib/db/schema/analytics";
import { links } from "@/lib/db/schema/links";
import { eq } from "drizzle-orm";
import { getClientIp } from "@/lib/utils/ip";
import { getCountryAndCity } from "@/lib/utils/geo";

export async function POST(request) {


try {
const body = await [Link]();
const { linkId } = body;

if (!linkId) {
return [Link](
{ error: "Link ID is required" },
{ status: 400 }
);
}

// Get link
const link = await [Link]({
where: eq([Link], parseInt(linkId)),
});

if (!link) {
return [Link](
{ error: "Link not found" },
{ status: 404 }
);
}

// Get IP and location


const ipAddress = getClientIp(request);
const { country, city } = await getCountryAndCity(ipAddress);

// Record click
await [Link](linkClicks).values({
linkId: parseInt(linkId),
userId: [Link],
clickedAt: new Date(),
ipAddress,
userAgent: [Link]("user-agent") || "",
referrer: [Link]("referer") || "",
country,
city,
});

// Increment link clicks


await [Link](links)
.set({
clicks: [Link] + 1,
})
.where(eq([Link], parseInt(linkId)));

return [Link]({ success: true });


} catch (error) {
[Link]("Error tracking click:", error);
return [Link](
{ error: "Failed to track click" },
{ status: 500 }
);
}
}

// src/app/api/analytics/track-view/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { pageViews } from "@/lib/db/schema/analytics";
import { getClientIp } from "@/lib/utils/ip";
import { getCountryAndCity } from "@/lib/utils/geo";

export async function POST(request) {


try {
const body = await [Link]();
const { userId } = body;

if (!userId) {
return [Link](
{ error: "User ID is required" },
{ status: 400 }
);
}

// Get IP and location


const ipAddress = getClientIp(request);
const { country, city } = await getCountryAndCity(ipAddress);

// Record page view


await [Link](pageViews).values({
userId: parseInt(userId),
viewedAt: new Date(),
ipAddress,
userAgent: [Link]("user-agent") || "",
referrer: [Link]("referer") || "",
country,
city,
});

return [Link]({ success: true });


} catch (error) {
[Link]("Error tracking page view:", error);
return [Link](
{ error: "Failed to track page view" },
{ status: 500 }
);
}
}

Create utility functions for IP and geolocation:


// src/lib/utils/[Link]
export function getClientIp(request) {
const forwarded = [Link]("x-forwarded-for");

if (forwarded) {
return [Link](",")[0].trim();
}

return "[Link]"; // Default for local development


}

// src/lib/utils/[Link]
export async function getCountryAndCity(ip) {
// For production, use a geolocation service like [Link] or maxmind
// This is a simplified example
try {
if (ip === "[Link]") {
return { country: "US", city: "Local" };
}

const response = await fetch(`[Link]


const data = await [Link]();

return {
country: [Link] || "",
city: [Link] || "",
};
} catch (error) {
[Link]("Error getting location:", error);
return { country: "", city: "" };
}
}

4.2 Theme Customization


Create the themes schema:
// src/lib/db/schema/[Link]
import { pgTable, serial, varchar, text, integer, boolean, jsonb } from 'driz
import { relations } from 'drizzle-orm';
import { users } from './users';

export const themes = pgTable('themes', {


id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
isDefault: boolean('is_default').default(false),
isPublic: boolean('is_public').default(true),
createdBy: integer('created_by').references(() => [Link]),
styles: jsonb('styles'),
});

export const themesRelations = relations(themes, ({ one, many }) => ({


creator: one(users, {
fields: [[Link]],
references: [[Link]],
}),
users: many(users),
}));

export const fonts = pgTable('fonts', {


id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
family: varchar('family', { length: 255 }).notNull(),
url: text('url'),
isDefault: boolean('is_default').default(false),
});

Create the theme API:


// src/app/api/themes/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { themes } from "@/lib/db/schema/themes";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { eq } from "drizzle-orm";

export async function GET(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
// For public themes, no authentication required
const publicThemes = await [Link]({
where: eq([Link], true),
});

return [Link](publicThemes);
}

// For authenticated users, include private themes created by them


const userThemes = await [Link]({
where: (themes) =>
eq([Link], true).or(
eq([Link], parseInt([Link]))
),
});

return [Link](userThemes);
} catch (error) {
[Link]("Error fetching themes:", error);
return [Link](
{ error: "Failed to fetch themes" },
{ status: 500 }
);
}
}

export async function POST(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await [Link]();

// Create theme
const newTheme = await [Link](themes).values({
name: [Link],
description: [Link],
isPublic: [Link] ?? false,
createdBy: parseInt([Link]),
styles: [Link] || {},
}).returning();

return [Link](newTheme[0], { status: 201 });


} catch (error) {
[Link]("Error creating theme:", error);
return [Link](
{ error: "Failed to create theme" },
{ status: 500 }
);
}
}

// src/app/api/themes/[id]/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { themes } from "@/lib/db/schema/themes";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { eq, and } from "drizzle-orm";
export async function GET(request, { params }) {
try {
const theme = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!theme) {
return [Link](
{ error: "Theme not found" },
{ status: 404 }
);
}

// If theme is not public, check if user is creator


if (![Link]) {
const session = await getServerSession(authOptions);

if (!session || [Link] !== parseInt([Link])) {


return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}
}

return [Link](theme);
} catch (error) {
[Link]("Error fetching theme:", error);
return [Link](
{ error: "Failed to fetch theme" },
{ status: 500 }
);
}
}

export async function PATCH(request, { params }) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}
const theme = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!theme) {
return [Link](
{ error: "Theme not found" },
{ status: 404 }
);
}

// Only creator or admin can update


if ([Link] !== parseInt([Link]) && [Link]
return [Link](
{ error: "Forbidden" },
{ status: 403 }
);
}

const body = await [Link]();

// Update theme
const updatedTheme = await [Link](themes)
.set({
name: [Link] ?? [Link],
description: [Link] ?? [Link],
isPublic: [Link] ?? [Link],
styles: [Link] ?? [Link],
})
.where(eq([Link], parseInt([Link])))
.returning();

return [Link](updatedTheme[0]);
} catch (error) {
[Link]("Error updating theme:", error);
return [Link](
{ error: "Failed to update theme" },
{ status: 500 }
);
}
}

export async function DELETE(request, { params }) {


try {
const session = await getServerSession(authOptions);
if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const theme = await [Link]({


where: eq([Link], parseInt([Link])),
});

if (!theme) {
return [Link](
{ error: "Theme not found" },
{ status: 404 }
);
}

// Only creator or admin can delete


if ([Link] !== parseInt([Link]) && [Link]
return [Link](
{ error: "Forbidden" },
{ status: 403 }
);
}

// Check if theme is default


if ([Link]) {
return [Link](
{ error: "Cannot delete default theme" },
{ status: 400 }
);
}

// Delete theme
await [Link](themes)
.where(eq([Link], parseInt([Link])));

return [Link]({ success: true });


} catch (error) {
[Link]("Error deleting theme:", error);
return [Link](
{ error: "Failed to delete theme" },
{ status: 500 }
);
}
}

Week 5: Billing & Subscriptions


5.1 Stripe Integration
First, install the Stripe package:

npm install stripe

Create the Stripe client:


// src/lib/[Link]
import Stripe from 'stripe';

export const stripe = new Stripe([Link].STRIPE_SECRET_KEY, {


apiVersion: '2023-10-16', // Use the latest API version
});

Create the subscription schema:


// src/lib/db/schema/[Link]
import { pgTable, serial, integer, varchar, timestamp, boolean, text, decim
import { relations } from 'drizzle-orm';
import { users } from './users';

export const plans = pgTable('plans', {


id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
price: decimal('price', { precision: 10, scale: 2 }).notNull(),
interval: varchar('interval', { length: 20 }).notNull(), // monthly, yearly
stripePriceId: varchar('stripe_price_id', { length: 255 }),
features: text('features'),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});

export const subscriptions = pgTable('subscriptions', {


id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => [Link]),
planId: integer('plan_id').notNull().references(() => [Link]),
stripeCustomerId: varchar('stripe_customer_id', { length: 255 }),
stripeSubscriptionId: varchar('stripe_subscription_id', { length: 255 }),
status: varchar('status', { length: 50 }).notNull(), // active, canceled, pas
currentPeriodStart: timestamp('current_period_start'),
currentPeriodEnd: timestamp('current_period_end'),
cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});

export const plansRelations = relations(plans, ({ many }) => ({


subscriptions: many(subscriptions),
}));

export const subscriptionsRelations = relations(subscriptions, ({ one }) =>


user: one(users, {
fields: [[Link]],
references: [[Link]],
}),
plan: one(plans, {
fields: [[Link]],
references: [[Link]],
}),
}));

Create the checkout API:


// src/app/api/billing/checkout/[Link]
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { db } from "@/lib/db";
import { plans } from "@/lib/db/schema/subscriptions";
import { users } from "@/lib/db/schema/users";
import { eq } from "drizzle-orm";
import { stripe } from "@/lib/stripe";

export async function POST(request) {


try {
const session = await getServerSession(authOptions);

if (!session) {
return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await [Link]();


const { planId } = body;

if (!planId) {
return [Link](
{ error: "Plan ID is required" },
{ status: 400 }
);
}

// Get plan
const plan = await [Link]({
where: eq([Link], parseInt(planId)),
});

if (!plan || ![Link]) {
return [Link](
{ error: "Plan not found or inactive" },
{ status: 404 }
);
}

// Get user
const user = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!user) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Create or get Stripe customer


let customerId = [Link];

if (!customerId) {
const customer = await [Link]({
email: [Link],
name: [Link],
metadata: {
userId: [Link],
},
});

customerId = [Link];

// Update user with Stripe customer ID


await [Link](users)
.set({ stripeCustomerId: customerId })
.where(eq([Link], parseInt([Link])));
}

// Create checkout session


const checkoutSession = await [Link]({
customer: customerId,
line_items: [
{
price: [Link],
quantity: 1,
},
],
mode: "subscription",
success_url: `${[Link].NEXT_PUBLIC_APP_URL}/dashboard?che
cancel_url: `${[Link].NEXT_PUBLIC_APP_URL}/plans?checkout=
metadata: {
userId: [Link],
planId: [Link],
},
});

return [Link]({ url: [Link] });


} catch (error) {
[Link]("Checkout error:", error);
return [Link](
{ error: "Failed to create checkout session" },
{ status: 500 }
);
}
}

Create the webhook handler for Stripe events:


// src/app/api/billing/webhook/[Link]
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema/subscriptions";
import { users } from "@/lib/db/schema/users";
import { eq } from "drizzle-orm";

export async function POST(request) {


const body = await [Link]();
const signature = headers().get("Stripe-Signature");
let event;

try {
event = [Link](
body,
signature,
[Link].STRIPE_WEBHOOK_SECRET
);
} catch (error) {
[Link]("Webhook signature verification failed:", error);
return [Link](
{ error: "Webhook signature verification failed" },
{ status: 400 }
);
}

try {
switch ([Link]) {
case "[Link]": {
const session = [Link];

// Handle successful checkout


await handleCheckoutCompleted(session);
break;
}

case "invoice.payment_succeeded": {
const invoice = [Link];

// Handle successful payment


await handlePaymentSucceeded(invoice);
break;
}

case "invoice.payment_failed": {
const invoice = [Link];

// Handle failed payment


await handlePaymentFailed(invoice);
break;
}

case "[Link]": {
const subscription = [Link];

// Handle subscription update


await handleSubscriptionUpdated(subscription);
break;
}

case "[Link]": {
const subscription = [Link];

// Handle subscription cancellation


await handleSubscriptionDeleted(subscription);
break;
}
}

return [Link]({ received: true });


} catch (error) {
[Link]("Webhook error:", error);
return [Link](
{ error: "Webhook handler failed" },
{ status: 500 }
);
}
}

async function handleCheckoutCompleted(session) {


const { userId, planId } = [Link];

if (!userId || !planId) {
[Link]("Missing metadata in checkout session");
return;
}

// Get subscription from Stripe


const stripeSubscription = await [Link](
[Link]
);

// Create subscription record


await [Link](subscriptions).values({
userId: parseInt(userId),
planId: parseInt(planId),
stripeCustomerId: [Link],
stripeSubscriptionId: [Link],
status: "active",
currentPeriodStart: new Date(stripeSubscription.current_period_start
currentPeriodEnd: new Date(stripeSubscription.current_period_end *
createdAt: new Date(),
updatedAt: new Date(),
});

// Update user's page status


await [Link](users)
.set({ pageStatus: true })
.where(eq([Link], parseInt(userId)));
}

async function handlePaymentSucceeded(invoice) {


const subscriptionId = [Link];

// Get subscription from database


const subscription = await [Link]({
where: eq([Link], subscriptionId),
});

if (!subscription) {
[Link]("Subscription not found:", subscriptionId);
return;
}

// Get subscription from Stripe


const stripeSubscription = await [Link](
subscriptionId
);

// Update subscription
await [Link](subscriptions)
.set({
status: "active",
currentPeriodStart: new Date(stripeSubscription.current_period_star
currentPeriodEnd: new Date(stripeSubscription.current_period_end
updatedAt: new Date(),
})
.where(eq([Link], subscriptionId));

// Update user's page status


await [Link](users)
.set({ pageStatus: true })
.where(eq([Link], [Link]));
}

async function handlePaymentFailed(invoice) {


const subscriptionId = [Link];
// Get subscription from database
const subscription = await [Link]({
where: eq([Link], subscriptionId),
});

if (!subscription) {
[Link]("Subscription not found:", subscriptionId);
return;
}

// Update subscription
await [Link](subscriptions)
.set({
status: "past_due",
updatedAt: new Date(),
})
.where(eq([Link], subscriptionId));
}

async function handleSubscriptionUpdated(subscription) {


// Get subscription from database
const dbSubscription = await [Link]({
where: eq([Link], [Link]),
});

if (!dbSubscription) {
[Link]("Subscription not found:", [Link]);
return;
}

// Update subscription
await [Link](subscriptions)
.set({
status: [Link],
currentPeriodStart: new Date(subscription.current_period_start * 10
currentPeriodEnd: new Date(subscription.current_period_end * 1000
cancelAtPeriodEnd: subscription.cancel_at_period_end,
updatedAt: new Date(),
})
.where(eq([Link], [Link]));
}

async function handleSubscriptionDeleted(subscription) {


// Get subscription from database
const dbSubscription = await [Link]({
where: eq([Link], [Link]),
});

if (!dbSubscription) {
[Link]("Subscription not found:", [Link]);
return;
}

// Update subscription
await [Link](subscriptions)
.set({
status: "canceled",
updatedAt: new Date(),
})
.where(eq([Link], [Link]));

// Update user's page status after grace period (3 days)


setTimeout(async () => {
await [Link](users)
.set({ pageStatus: false })
.where(eq([Link], [Link]));
}, 3 * 24 * 60 * 60 * 1000); // 3 days in milliseconds
}

Phase 3: Admin Panel


Week 6: Admin Dashboard
6.1 Admin Layout
Create the admin layout:

// src/app/(admin)/[Link]
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { redirect } from "next/navigation";
import AdminSidebar from "@/components/admin/AdminSidebar";
import AdminHeader from "@/components/admin/AdminHeader";

export default async function AdminLayout({ children }) {


const session = await getServerSession(authOptions);

if (!session || [Link] !== "admin") {


redirect("/login");
}

return (
<div className="flex h-screen bg-gray-50">
<AdminSidebar />
<div className="flex flex-1 flex-col overflow-hidden">
<AdminHeader user={[Link]} />
<main className="flex-1 overflow-y-auto p-4 md:p-6">{children}</
</div>
</div>
);
}

Create the admin components:


// src/components/admin/[Link]
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
Users,
Settings,
CreditCard,
Palette,
FileText,
Globe,
LogOut
} from "lucide-react";
import { signOut } from "next-auth/react";

export default function AdminSidebar() {


const pathname = usePathname();

const navItems = [
{
name: "Dashboard",
href: "/admin/dashboard",
icon: LayoutDashboard,
},
{
name: "Users",
href: "/admin/users",
icon: Users,
},
{
name: "Plans",
href: "/admin/plans",
icon: CreditCard,
},
{
name: "Subscriptions",
href: "/admin/subscriptions",
icon: CreditCard,
},
{
name: "Themes",
href: "/admin/themes",
icon: Palette,
},
{
name: "Pages",
href: "/admin/pages",
icon: FileText,
},
{
name: "Languages",
href: "/admin/languages",
icon: Globe,
},
{
name: "Settings",
href: "/admin/settings",
icon: Settings,
},
];

return (
<aside className="hidden w-64 flex-shrink-0 bg-white shadow-md m
<div className="flex h-16 items-center justify-center border-b">
<Link href="/admin/dashboard" className="text-xl font-bold">
OnlyLinks Admin
</Link>
</div>
<nav className="flex flex-1 flex-col p-4">
<ul className="space-y-1">
{[Link]((item) => {
const isActive = pathname === [Link];
return (
<li key={[Link]}>
<Link
href={[Link]}
className={`flex items-center rounded-md px-4 py-2 text-sm
isActive
? "bg-blue-50 text-blue-700"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<[Link] className="mr-3 h-5 w-5" />
{[Link]}
</Link>
</li>
);
})}
</ul>
<div className="mt-auto">
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="flex w-full items-center rounded-md px-4 py-2 text-s
>
<LogOut className="mr-3 h-5 w-5" />
Sign out
</button>
</div>
</nav>
</aside>
);
}

// src/components/admin/[Link]
import { useState } from "react";
import { Menu, Bell, User } from "lucide-react";
import Link from "next/link";
import Image from "next/image";
import { signOut } from "next-auth/react";

export default function AdminHeader({ user }) {


const [isProfileOpen, setIsProfileOpen] = useState(false);

return (
<header className="flex h-16 items-center justify-between border-b
<button className="rounded-md p-2 text-gray-500 hover:bg-gray-10
<Menu className="h-6 w-6" />
</button>

<div className="ml-auto flex items-center space-x-4">


<button className="rounded-md p-2 text-gray-500 hover:bg-gray-1
<Bell className="h-5 w-5" />
</button>

<div className="relative">
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center space-x-2 rounded-md p-2 text-gra
>
<div className="relative h-8 w-8 overflow-hidden rounded-full b
{[Link] ? (
<Image
src={[Link]}
alt={[Link] || "Admin"}
fill
className="object-cover"
/>
):(
<User className="h-full w-full p-1" />
)}
</div>
<span className="hidden text-sm font-medium md:block">
{[Link] || [Link]}
</span>
</button>

{isProfileOpen && (
<div className="absolute right-0 mt-2 w-48 rounded-md bg-whit
<Link
href="/admin/settings/profile"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gra
onClick={() => setIsProfileOpen(false)}
>
Your Profile
</Link>
<Link
href="/admin/settings"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gra
onClick={() => setIsProfileOpen(false)}
>
Settings
</Link>
<button
onClick={() => {
setIsProfileOpen(false);
signOut({ callbackUrl: "/" });
}}
className="block w-full px-4 py-2 text-left text-sm text-gray-70
>
Sign out
</button>
</div>
)}
</div>
</div>
</header>
);
}

6.2 User Management


Create the admin users API:
// src/app/api/admin/users/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { desc, asc, sql } from "drizzle-orm";

export async function GET(request) {


try {
const session = await getServerSession(authOptions);

if (!session || [Link] !== "admin") {


return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const { searchParams } = new URL([Link]);


const page = parseInt([Link]("page") || "1");
const limit = parseInt([Link]("limit") || "10");
const search = [Link]("search") || "";
const sortBy = [Link]("sortBy") || "createdAt";
const sortOrder = [Link]("sortOrder") || "desc";

const offset = (page - 1) * limit;

// Build query
let query = [Link]({
id: [Link],
name: [Link],
email: [Link],
username: [Link],
active: [Link],
role: [Link],
createdAt: [Link],
pageStatus: [Link],
}).from(users);
// Add search condition
if (search) {
query = [Link](
sql`${[Link]} ILIKE ${'%' + search + '%'} OR ${[Link]} ILIKE
);
}

// Add sorting
if (sortOrder === "desc") {
query = [Link](desc(users[sortBy]));
} else {
query = [Link](asc(users[sortBy]));
}

// Add pagination
query = [Link](limit).offset(offset);

// Execute query
const usersList = await query;

// Get total count


const countQuery = [Link]({ count: sql`COUNT(*)` }).from(users);

if (search) {
[Link](
sql`${[Link]} ILIKE ${'%' + search + '%'} OR ${[Link]} ILIKE
);
}

const [{ count }] = await countQuery;


const totalPages = [Link](count / limit);

return [Link]({
users: usersList,
pagination: {
page,
limit,
totalItems: count,
totalPages,
},
});
} catch (error) {
[Link]("Error fetching users:", error);
return [Link](
{ error: "Failed to fetch users" },
{ status: 500 }
);
}
}

Create the admin user detail API:


// src/app/api/admin/users/[id]/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema/users";
import { subscriptions } from "@/lib/db/schema/subscriptions";
import { links } from "@/lib/db/schema/links";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/auth-options";
import { eq } from "drizzle-orm";
import bcrypt from "bcrypt";

export async function GET(request, { params }) {


try {
const session = await getServerSession(authOptions);

if (!session || [Link] !== "admin") {


return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Get user
const user = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!user) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Don't return password


const { password, ...userWithoutPassword } = user;

// Get user's subscription


const subscription = await [Link]({
where: eq([Link], parseInt([Link])),
with: {
plan: true,
},
});

// Get user's links count


const linksCount = await [Link]({ count: sql`COUNT(*)` })
.from(links)
.where(eq([Link], parseInt([Link])));

return [Link]({
user: userWithoutPassword,
subscription,
linksCount: linksCount[0].count,
});
} catch (error) {
[Link]("Error fetching user:", error);
return [Link](
{ error: "Failed to fetch user" },
{ status: 500 }
);
}
}

export async function PATCH(request, { params }) {


try {
const session = await getServerSession(authOptions);

if (!session || [Link] !== "admin") {


return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

const body = await [Link]();

// Check if user exists


const existingUser = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!existingUser) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Prepare update data


const updateData = { ...body };

// Hash password if provided


if ([Link]) {
[Link] = await [Link]([Link], 10
}

// Update user
const updatedUser = await [Link](users)
.set({
...updateData,
updatedAt: new Date(),
})
.where(eq([Link], parseInt([Link])))
.returning();

// Don't return password


const { password, ...userWithoutPassword } = updatedUser[0];

return [Link](userWithoutPassword);
} catch (error) {
[Link]("Error updating user:", error);
return [Link](
{ error: "Failed to update user" },
{ status: 500 }
);
}
}

export async function DELETE(request, { params }) {


try {
const session = await getServerSession(authOptions);

if (!session || [Link] !== "admin") {


return [Link](
{ error: "Unauthorized" },
{ status: 401 }
);
}

// Check if user exists


const existingUser = await [Link]({
where: eq([Link], parseInt([Link])),
});

if (!existingUser) {
return [Link](
{ error: "User not found" },
{ status: 404 }
);
}

// Don't allow deleting admin users


if ([Link] === "admin") {
return [Link](
{ error: "Cannot delete admin users" },
{ status: 400 }
);
}

// Delete user
await [Link](users)
.where(eq([Link], parseInt([Link])));

return [Link]({ success: true });


} catch (error) {
[Link]("Error deleting user:", error);
return [Link](
{ error: "Failed to delete user" },
{ status: 500 }
);
}
}

Phase 4: Testing & Optimization


Week 9: Testing
9.1 Unit Testing
Install testing libraries:

npm install -D jest @testing-library/react @testing-library/jest-dom jest-

Configure Jest:
// [Link]
const nextJest = require('next/jest');

const createJestConfig = nextJest({


dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/[Link]'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};

[Link] = createJestConfig(customJestConfig);

// [Link]
import '@testing-library/jest-dom';

Create a sample test:


// src/components/ui/[Link]
import { render, screen } from '@testing-library/react';
import Button from './Button';

describe('Button component', () => {


it('renders correctly', () => {
render(<Button>Click me</Button>);
expect([Link]('Click me')).toBeInTheDocument();
});

it('applies the correct variant class', () => {


render(<Button variant="primary">Primary Button</Button>);
const button = [Link]('Primary Button');
expect(button).toHaveClass('bg-blue-600');
expect(button).toHaveClass('text-white');
});

it('handles onClick events', () => {


const handleClick = [Link]();
render(<Button onClick={handleClick}>Click me</Button>);
[Link]('Click me').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('can be disabled', () => {


render(<Button disabled>Disabled Button</Button>);
expect([Link]('Disabled Button')).toBeDisabled();
});
});

9.2 API Testing


Create API tests:
// src/app/api/users/[Link]
import { createMocks } from 'node-mocks-http';
import { POST } from './route';
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema/users';
import bcrypt from 'bcrypt';

// Mock dependencies
[Link]('@/lib/db', () => ({
insert: [Link](),
query: {
users: {
findFirst: [Link](),
},
},
}));

[Link]('bcrypt', () => ({
hash: [Link](() => 'hashed_password'),
}));

describe('Users API', () => {


beforeEach(() => {
[Link]();
});

describe('POST /api/users', () => {


it('should create a new user', async () => {
// Mock database responses
[Link](null); // No existing
[Link](() => ({
values: () => ({
returning: () => [Link]([{
id: 1,
name: 'Test User',
email: 'test@[Link]',
username: 'testuser',
password: 'hashed_password',
}]),
}),
}));

// Create mock request


const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Test User',
email: 'test@[Link]',
password: 'password123',
username: 'testuser',
},
});

// Call API handler


await POST(req, res);

// Assertions
expect([Link]).toHaveBeenCalledTimes(2); // Check
expect([Link]).toHaveBeenCalledWith('password123', 10);
expect([Link]).toHaveBeenCalledTimes(1);
expect(res._getStatusCode()).toBe(201);

const responseData = [Link](res._getData());


expect(responseData).toEqual({
id: 1,
name: 'Test User',
email: 'test@[Link]',
username: 'testuser',
});
});

it('should return 400 if email already exists', async () => {


// Mock existing user with same email
[Link]({
id: 1,
email: 'test@[Link]',
});

// Create mock request


const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Test User',
email: 'test@[Link]',
password: 'password123',
username: 'testuser',
},
});

// Call API handler


await POST(req, res);
// Assertions
expect(res._getStatusCode()).toBe(400);
expect([Link](res._getData())).toEqual({
error: 'Email already in use',
});
});
});
});

Week 10: Deployment Preparation


10.1 SEO Optimization
Create a metadata utility:
// src/lib/utils/[Link]
export function generateMetadata({
title,
description,
keywords,
image,
url,
type = 'website',
}) {
const baseUrl = [Link].NEXT_PUBLIC_APP_URL;

return {
title: title ? `${title} | OnlyLinks` : 'OnlyLinks - Manage Your Links',
description: description || 'Create and manage your links in one place
keywords: keywords || 'links, bio links, link management, social media
openGraph: {
title: title ? `${title} | OnlyLinks` : 'OnlyLinks - Manage Your Links',
description: description || 'Create and manage your links in one plac
url: url ? `${baseUrl}${url}` : baseUrl,
siteName: 'OnlyLinks',
images: [
{
url: image || `${baseUrl}/images/[Link]`,
width: 1200,
height: 630,
alt: title || 'OnlyLinks',
},
],
locale: 'en_US',
type,
},
twitter: {
card: 'summary_large_image',
title: title ? `${title} | OnlyLinks` : 'OnlyLinks - Manage Your Links',
description: description || 'Create and manage your links in one plac
images: [image || `${baseUrl}/images/[Link]`],
},
};
}

Create a sitemap generator:


// src/app/[Link]
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema/users';
import { eq } from 'drizzle-orm';

export default async function sitemap() {


const baseUrl = [Link].NEXT_PUBLIC_APP_URL;

// Static routes
const routes = [
{
url: `${baseUrl}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
{
url: `${baseUrl}/login`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/signup`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/plans`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
];

// Get public user profiles


const publicUsers = await [Link]({
where: eq([Link], true),
select: {
username: true,
updatedAt: true,
},
});

// Add user profiles to sitemap


const userRoutes = [Link]((user) => ({
url: `${baseUrl}/${[Link]}`,
lastModified: [Link],
changeFrequency: 'daily',
priority: 0.7,
}));

return [...routes, ...userRoutes];


}

Create a [Link] file:


// src/app/[Link]
export default function robots() {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/dashboard/'],
},
sitemap: `${[Link].NEXT_PUBLIC_APP_URL}/[Link]`,
};
}

10.2 Accessibility
Install accessibility testing tools:
npm install -D axe-core @axe-core/react

Set up accessibility testing:


// src/lib/utils/[Link]
import React from 'react';
import ReactDOM from 'react-dom';
import { axe, toHaveNoViolations } from 'jest-axe';

[Link](toHaveNoViolations);

export async function checkA11y(component) {


const container = [Link]('div');
[Link](container);
[Link](component, container);

const results = await axe(container);


expect(results).toHaveNoViolations();

[Link](container);
[Link]();
}

Create an accessibility test:


// src/components/ui/[Link]
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Button from './Button';

[Link](toHaveNoViolations);

describe('Button accessibility', () => {


it('should not have accessibility violations', async () => {
const { container } = render(<Button>Accessible Button</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it('should have proper ARIA attributes when loading', async () => {


const { container, getByText } = render(<Button isLoading>Loading</Bu
expect(getByText('Loading')).toHaveAttribute('aria-busy', 'true');
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});

Phase 5: Launch & Migration


Week 11: Deployment
11.1 Vercel Deployment Configuration
Create a Vercel configuration file:

// [Link]
{
"version": 2,
"buildCommand": "npm run build",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["iad1"],
"env": {
"NEXT_PUBLIC_APP_URL": "[Link]
},
"crons": [
{
"path": "/api/cron/subscriptions",
"schedule": "0 0 * * *"
},
{
"path": "/api/cron/sessions",
"schedule": "0 */12 * * *"
}
]
}

Create cron job handlers:


// src/app/api/cron/subscriptions/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema/subscriptions";
import { users } from "@/lib/db/schema/users";
import { eq, and, lte } from "drizzle-orm";

export async function GET(request) {


try {
// Verify cron secret to prevent unauthorized access
const authHeader = [Link]("authorization");
if (authHeader !== `Bearer ${[Link].CRON_SECRET}`) {
return [Link]({ error: "Unauthorized" }, { status: 401 });
}

// Get expired subscriptions


const expiredSubscriptions = await [Link]({
where: and(
eq([Link], "active"),
lte([Link], new Date())
),
});

// Process each expired subscription


for (const subscription of expiredSubscriptions) {
// Update subscription status
await [Link](subscriptions)
.set({
status: "expired",
updatedAt: new Date(),
})
.where(eq([Link], [Link]));

// Update user page status after grace period (3 days)


const gracePeriod = new Date();
[Link]([Link]() - 3);

if ([Link] <= gracePeriod) {


await [Link](users)
.set({ pageStatus: false })
.where(eq([Link], [Link]));
}
}

return [Link]({
success: true,
processed: [Link],
});
} catch (error) {
[Link]("Subscription cron error:", error);
return [Link](
{ error: "Failed to process subscriptions" },
{ status: 500 }
);
}
}

// src/app/api/cron/sessions/[Link]
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { sessions } from "@/lib/db/schema/sessions";
import { lte } from "drizzle-orm";

export async function GET(request) {


try {
// Verify cron secret to prevent unauthorized access
const authHeader = [Link]("authorization");
if (authHeader !== `Bearer ${[Link].CRON_SECRET}`) {
return [Link]({ error: "Unauthorized" }, { status: 401 });
}

// Delete expired sessions


const result = await [Link](sessions)
.where(lte([Link], new Date()));
return [Link]({
success: true,
deleted: [Link],
});
} catch (error) {
[Link]("Sessions cleanup error:", error);
return [Link](
{ error: "Failed to clean up sessions" },
{ status: 500 }
);
}
}

11.2 Data Migration Scripts


Create a migration script for users:
// scripts/[Link]
require('dotenv').config();
const { Pool } = require('pg');
const bcrypt = require('bcrypt');

// Source database (PHP app)


const sourcePool = new Pool({
connectionString: [Link].SOURCE_DATABASE_URL,
});

// Target database (NextJS app)


const targetPool = new Pool({
connectionString: [Link].DATABASE_URL,
});

async function migrateUsers() {


[Link]('Starting user migration...');

try {
// Get users from source database
const { rows: sourceUsers } = await [Link]('SELECT * FROM
[Link](`Found ${[Link]} users to migrate`);

// Migrate each user


for (const user of sourceUsers) {
// Check if user already exists in target database
const { rows: existingUsers } = await [Link](
'SELECT * FROM users WHERE email = $1',
[[Link]]
);
if ([Link] > 0) {
[Link](`User ${[Link]} already exists, skipping`);
continue;
}

// Insert user into target database


await [Link](
`INSERT INTO users (
name, email, password, username, active, created_at, updated_at,
role, profile_image, page_status, stripe_customer_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
[
[Link],
[Link],
[Link], // Assuming passwords are already hashed
[Link],
[Link],
user.created_at,
user.updated_at,
[Link] || 'user',
user.profile_image,
user.page_status,
user.stripe_customer_id,
]
);

[Link](`Migrated user: ${[Link]}`);


}

[Link]('User migration completed successfully');


} catch (error) {
[Link]('Error migrating users:', error);
} finally {
// Close database connections
await [Link]();
await [Link]();
}
}

migrateUsers();
nical Implementation Guide

ructions for migrating the OnlyLinks PHP application to NextJS using Drizzle ORM. Each phase is broken down into specific im

--typescript --eslint --app --src-dir --import-alias "@/*"

rm/resolvers

windcss husky lint-staged


timestamp, text } from 'drizzle-orm/pg-core';

Null().unique(),
55 }).notNull(),

100 }).unique(),
ers, ({ many }) => ({

s (links, subscriptions, plans, etc.).

h/providers/credentials";
password" }

.password) {
uth-options";

resolvers/zod";
st be at least 6 characters"),

: { errors } } = useForm({

col items-center justify-center px-4">


ce-y-8 rounded-lg border p-6 shadow-md">

Login to OnlyLinks</h1>
Enter your credentials to access your account</p>
d-50 p-4 text-sm text-red-700">

bmit)} className="mt-8 space-y-6">

="block text-sm font-medium">

unded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"

-red-600">{[Link]}</p>

ame="block text-sm font-medium">

unded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"

-red-600">{[Link]}</p>

stify-between">
der-gray-300 text-blue-600 focus:ring-blue-500"

assName="ml-2 block text-sm text-gray-900">

ssName="font-medium text-blue-600 hover:text-blue-500">

er rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none fo

ont-medium text-blue-600 hover:text-blue-500">


least 2 characters"),

st be at least 6 characters"),
ust be at least 3 characters"),

e } = body;

[Link]({

[Link]({
ash(password, 10);

uth-options";
authOptions);

data or admins to access any data


[Link] !== "admin") {

rd } = user;
authOptions);

data or admins to update any data


[Link] !== "admin") {

rd } = updatedUser[0];
"/login", [Link]));

"/admin") && [Link] !== "admin") {


"/dashboard", [Link]));
required" },

mpare(password, [Link]);
uccess: true });
dminToken, {

roduction",

uth-options";

authOptions);
mage/png", "image/gif"];

[Link](/\s/g, "-")}`;
ublic", "uploads");
uth-options";

hboard/Sidebar";
hboard/Header";

ayout({ children }) {
uthOptions);

erflow-hidden">

auto p-4 md:p-6">{children}</main>


rink-0 bg-white shadow-md md:flex md:flex-col">
r justify-center border-b">
text-xl font-bold">OnlyLinks</Link>

ounded-md px-4 py-2 text-sm font-medium ${


r rounded-md px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"

useState(false);

nter justify-between border-b bg-white px-4 md:px-6">


ext-gray-500 hover:bg-gray-100 md:hidden">

nter space-x-4">
text-gray-500 hover:bg-gray-100">
rofileOpen)}
e-x-2 rounded-md p-2 text-gray-700 hover:bg-gray-100"

verflow-hidden rounded-full bg-gray-200">

ont-medium md:block">

mt-2 w-48 rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5">

sm text-gray-700 hover:bg-gray-100"

sm text-gray-700 hover:bg-gray-100"

2 text-left text-sm text-gray-700 hover:bg-gray-100"


stamp, integer, boolean } from 'drizzle-orm/pg-core';

nces(() => [Link]),

s, ({ one }) => ({
uth-options";

authOptions);

authOptions);
[Link]({

? [Link] + 1 : 0;

status: 201 });


ove } from "lucide-react";
aggable } from "react-beautiful-dnd";

[Link], 1);
reorderedItem);

index) => ({
[Link]];

[Link] }),

elete this link?")) return;


er justify-center">

pin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>


Loading links...</p>

er justify-center">

lue-600 px-4 py-2 text-white hover:bg-blue-700"

tify-between">
our Links</h1>

ed-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"


y-50 p-8 text-center">
don't have any links yet.</p>

lue-600 px-4 py-2 text-white hover:bg-blue-700"

eDragEnd}>

rder p-4 ${

enter justify-between">

move text-gray-400 hover:text-gray-600"


dium">{[Link]}</h3>

enter text-sm text-gray-500 hover:text-blue-600"

="ml-1 h-3 w-3" />

center space-x-2">

ext-sm text-gray-500">

e inline-flex cursor-pointer items-center">

[Link], [Link])

6 w-11 rounded-full bg-gray-200 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:b

h(`/links/${[Link]}`)}
p-2 text-gray-500 hover:bg-gray-100 hover:text-blue-600"

teLink([Link])}
p-2 text-gray-500 hover:bg-gray-100 hover:text-red-600"
p, varchar, text } from 'drizzle-orm/pg-core';

ces(() => [Link]),


nces(() => [Link]),

(linkClicks, ({ one }) => ({


nces(() => [Link]),

ns(pageViews, ({ one }) => ({

b/schema/analytics";
hema/links";

m "@/lib/utils/geo";
yAndCity(ipAddress);

agent") || "",

ma/analytics";

utils/geo";
yAndCity(ipAddress);

agent") || "",

x-forwarded-for");

velopment
ce like [Link] or maxmind

[Link]/${ip}/json?token=${[Link].IPINFO_TOKEN}`);

ger, boolean, jsonb } from 'drizzle-orm/pg-core';

es(() => [Link]),

hemes, ({ one, many }) => ({


uth-options";

authOptions);

[Link]({

te themes created by them


[Link]({

[Link]))
authOptions);

mes).values({

], { status: 201 });

uth-options";
(authOptions);

seInt([Link])) {

authOptions);
[Link]) && [Link] !== "admin") {

[Link],

authOptions);
[Link]) && [Link] !== "admin") {
nv.STRIPE_SECRET_KEY, {
t API version

timestamp, boolean, text, decimal } from 'drizzle-orm/pg-core';

e: 2 }).notNull(),
notNull(), // monthly, yearly
ength: 255 }),

criptions', {

nces(() => [Link]),


nces(() => [Link]),
mer_id', { length: 255 }),
cription_id', { length: 255 }),
Null(), // active, canceled, past_due
period_start'),
eriod_end'),
period_end').default(false),
ns, ({ many }) => ({

tions(subscriptions, ({ one }) => ({

uth-options";

ubscriptions";

authOptions);
[Link]({

LIC_APP_URL}/dashboard?checkout=success`,
C_APP_URL}/plans?checkout=canceled`,

[Link] });

hema/subscriptions";
cation failed:", error);
bscription);

ckout session");

[Link](

scription.current_period_start * 1000),
cription.current_period_end * 1000),
[Link]({
onId, subscriptionId),

subscriptionId);

[Link](

bscription.current_period_start * 1000),
scription.current_period_end * 1000),

onId, subscriptionId));
[Link]({
onId, subscriptionId),

subscriptionId);

onId, subscriptionId));

d(subscription) {

[Link]({
onId, [Link]),

[Link]);

tion.current_period_start * 1000),
on.current_period_end * 1000),
_at_period_end,

onId, [Link]));

(subscription) {

[Link]({
onId, [Link]),
[Link]);

onId, [Link]));

eriod (3 days)

uth-options";

ts/admin/AdminSidebar";
ts/admin/AdminHeader";

t({ children }) {
uthOptions);
erflow-hidden">

auto p-4 md:p-6">{children}</main>


rink-0 bg-white shadow-md md:flex md:flex-col">
r justify-center border-b">
Name="text-xl font-bold">

ounded-md px-4 py-2 text-sm font-medium ${


r rounded-md px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"

useState(false);

nter justify-between border-b bg-white px-4 md:px-6">


ext-gray-500 hover:bg-gray-100 md:hidden">

nter space-x-4">
text-gray-500 hover:bg-gray-100">

rofileOpen)}
e-x-2 rounded-md p-2 text-gray-700 hover:bg-gray-100"
verflow-hidden rounded-full bg-gray-200">

ont-medium md:block">

mt-2 w-48 rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5">

sm text-gray-700 hover:bg-gray-100"

sm text-gray-700 hover:bg-gray-100"

2 text-left text-sm text-gray-700 hover:bg-gray-100"


uth-options";

authOptions);

("page") || "1");
"limit") || "10");

y") || "createdAt";
rtOrder") || "desc";
+ '%'} OR ${[Link]} ILIKE ${'%' + search + '%'} OR ${[Link]} ILIKE ${'%' + search + '%'}`

ql`COUNT(*)` }).from(users);

+ '%'} OR ${[Link]} ILIKE ${'%' + search + '%'} OR ${[Link]} ILIKE ${'%' + search + '%'}`
hema/subscriptions";

uth-options";

authOptions);

rd } = user;

[Link]({
nt([Link])),
nt: sql`COUNT(*)` })

authOptions);

[Link]({
ash([Link], 10);

rd } = updatedUser[0];

authOptions);

[Link]({
@testing-library/jest-dom jest-environment-jsdom
mJestConfig);

brary/react';

eInTheDocument();

ary Button</Button>);
y Button');

lick me</Button>);

n</Button>);
on')).toBeDisabled();
ValueOnce(null); // No existing user
eBeenCalledTimes(2); // Check email and username
With('password123', 10);

getData());

sts', async () => {

ValueOnce({
C_APP_URL;

nks - Manage Your Links',


manage your links in one place with OnlyLinks.',
ink management, social media',

Links - Manage Your Links',


manage your links in one place with OnlyLinks.',

[Link]`,
Links - Manage Your Links',
manage your links in one place with OnlyLinks.',
[Link]`],

C_APP_URL;

.findMany({
APP_URL}/[Link]`,
container);

', async () => {


cessible Button</Button>);

en loading', async () => {


Button isLoading>Loading</Button>);
ibute('aria-busy', 'true');
[Link]"

hema/subscriptions";

rized access
("authorization");
v.CRON_SECRET}`) {
authorized" }, { status: 401 });

[Link]({
period (3 days)

acePeriod) {

a/sessions";

rized access
("authorization");
v.CRON_SECRET}`) {
authorized" }, { status: 401 });
DATABASE_URL,

[Link]('SELECT * FROM users');


h} users to migrate`);

[Link](
y exists, skipping`);

ctive, created_at, updated_at,


pe_customer_id
8, $9, $10, $11)`,

ds are already hashed

uccessfully');
is broken down into specific implementation steps with code examples.
none focus:ring-blue-500"

none focus:ring-blue-500"
blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked
fter:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:
after:border-white peer-focus:outline-none"></div>

You might also like