footer+responsivenes
This commit is contained in:
parent
7afc865a3f
commit
91313ac7d5
|
|
@ -33,6 +33,10 @@ ENV IP_SALT="build-time-salt"
|
||||||
ENV STRIPE_SECRET_KEY="sk_test_placeholder_for_build"
|
ENV STRIPE_SECRET_KEY="sk_test_placeholder_for_build"
|
||||||
ENV RESEND_API_KEY="re_placeholder_for_build"
|
ENV RESEND_API_KEY="re_placeholder_for_build"
|
||||||
ENV NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
ENV NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
||||||
|
# PostHog Analytics - REQUIRED at build time for client-side bundle
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_KEY="phc_97JBJVVQlqqiZuTVRHuBnnG9HasOv3GSsdeVjossizJ"
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_HOST="https://us.i.posthog.com"
|
||||||
|
ENV NEXT_PUBLIC_INDEXABLE="true"
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||||
|
import { Footer } from '@/components/ui/Footer';
|
||||||
import { useTranslation } from '@/hooks/useTranslation';
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
plan: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AppLayout({
|
export default function AppLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -16,6 +24,24 @@ export default function AppLayout({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
// Fetch user data on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user');
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
setUser(userData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
// Track logout event before clearing data
|
// Track logout event before clearing data
|
||||||
|
|
@ -37,6 +63,34 @@ export default function AppLayout({
|
||||||
router.push('/');
|
router.push('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
|
||||||
|
const getUserInitials = () => {
|
||||||
|
if (!user) return 'U';
|
||||||
|
|
||||||
|
if (user.name) {
|
||||||
|
const names = user.name.trim().split(' ');
|
||||||
|
if (names.length >= 2) {
|
||||||
|
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
|
||||||
|
}
|
||||||
|
return user.name.substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to email
|
||||||
|
return user.email.substring(0, 1).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get display name (first name or full name)
|
||||||
|
const getDisplayName = () => {
|
||||||
|
if (!user) return 'User';
|
||||||
|
|
||||||
|
if (user.name) {
|
||||||
|
return user.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to email without domain
|
||||||
|
return user.email.split('@')[0];
|
||||||
|
};
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{
|
{
|
||||||
name: t('nav.dashboard'),
|
name: t('nav.dashboard'),
|
||||||
|
|
@ -169,11 +223,11 @@ export default function AppLayout({
|
||||||
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
||||||
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||||
<span className="text-sm font-medium text-primary-600">
|
<span className="text-sm font-medium text-primary-600">
|
||||||
U
|
{getUserInitials()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden md:block font-medium">
|
<span className="hidden md:block font-medium">
|
||||||
User
|
{getDisplayName()}
|
||||||
</span>
|
</span>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
|
@ -193,6 +247,9 @@ export default function AppLayout({
|
||||||
<main className="p-6">
|
<main className="p-6">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@ export async function POST(request: NextRequest) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SECURITY: Only allow support@qrmaster.net to access newsletter admin
|
||||||
|
const ALLOWED_ADMIN_EMAIL = 'support@qrmaster.net';
|
||||||
|
if (email.toLowerCase() !== ALLOWED_ADMIN_EMAIL) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Access denied. Only authorized accounts can access this area.' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if user exists
|
// Check if user exists
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findUnique({
|
||||||
where: { email: email.toLowerCase() },
|
where: { email: email.toLowerCase() },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/user
|
||||||
|
* Get current user information
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const userId = cookies().get('userId')?.value;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
plan: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Internal server error' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-gray-900 text-white py-12 mt-20">
|
||||||
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
|
||||||
|
<div className="grid md:grid-cols-4 gap-8">
|
||||||
|
<div>
|
||||||
|
<Link href="/" className="flex items-center space-x-2 mb-4 hover:opacity-80 transition-opacity">
|
||||||
|
<img src="/logo.svg" alt="QR Master" className="w-10 h-10" />
|
||||||
|
<span className="text-xl font-bold">QR Master</span>
|
||||||
|
</Link>
|
||||||
|
<p className="text-gray-400">
|
||||||
|
Create custom QR codes in seconds with advanced tracking and analytics.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-4">Product</h3>
|
||||||
|
<ul className="space-y-2 text-gray-400">
|
||||||
|
<li><Link href="/#features" className="hover:text-white">Features</Link></li>
|
||||||
|
<li><Link href="/#pricing" className="hover:text-white">Pricing</Link></li>
|
||||||
|
<li><Link href="/#faq" className="hover:text-white">FAQ</Link></li>
|
||||||
|
<li><Link href="/blog" className="hover:text-white">Blog</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-4">Resources</h3>
|
||||||
|
<ul className="space-y-2 text-gray-400">
|
||||||
|
<li><Link href="/pricing" className="hover:text-white">Full Pricing</Link></li>
|
||||||
|
<li><Link href="/faq" className="hover:text-white">All Questions</Link></li>
|
||||||
|
<li><Link href="/blog" className="hover:text-white">Blog</Link></li>
|
||||||
|
<li><Link href="/signup" className="hover:text-white">Get Started</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold mb-4">Legal</h3>
|
||||||
|
<ul className="space-y-2 text-gray-400">
|
||||||
|
<li><Link href="/privacy" className="hover:text-white">Privacy Policy</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-800 mt-8 pt-8 flex items-center justify-between text-gray-400">
|
||||||
|
<Link
|
||||||
|
href="/newsletter"
|
||||||
|
className="text-xs hover:text-white transition-colors flex items-center gap-1.5 opacity-50 hover:opacity-100"
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||||
|
</svg>
|
||||||
|
Admin
|
||||||
|
</Link>
|
||||||
|
<p>© 2025 QR Master. All rights reserved.</p>
|
||||||
|
<div className="w-12"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,28 @@ import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
export default withAuth(
|
export default withAuth(
|
||||||
function middleware(req) {
|
function middleware(req) {
|
||||||
|
const token = req.nextauth.token;
|
||||||
|
const path = req.nextUrl.pathname;
|
||||||
|
|
||||||
|
// Protected dashboard routes - redirect to /signup if not authenticated
|
||||||
|
const protectedRoutes = [
|
||||||
|
'/dashboard',
|
||||||
|
'/create',
|
||||||
|
'/bulk-creation',
|
||||||
|
'/analytics',
|
||||||
|
'/pricing',
|
||||||
|
'/settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if current path matches any protected route
|
||||||
|
const isProtectedRoute = protectedRoutes.some(route => path.startsWith(route));
|
||||||
|
|
||||||
|
// If protected route and no token, redirect to signup
|
||||||
|
if (isProtectedRoute && !token) {
|
||||||
|
const signupUrl = new URL('/signup', req.url);
|
||||||
|
return NextResponse.redirect(signupUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue