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 RESEND_API_KEY="re_placeholder_for_build"
|
||||
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 npm run build
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
||||
import { Footer } from '@/components/ui/Footer';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
plan: string | null;
|
||||
}
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
}: {
|
||||
|
|
@ -16,6 +24,24 @@ export default function AppLayout({
|
|||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
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 () => {
|
||||
// Track logout event before clearing data
|
||||
|
|
@ -37,6 +63,34 @@ export default function AppLayout({
|
|||
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 = [
|
||||
{
|
||||
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">
|
||||
<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">
|
||||
U
|
||||
{getUserInitials()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="hidden md:block font-medium">
|
||||
User
|
||||
{getDisplayName()}
|
||||
</span>
|
||||
<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" />
|
||||
|
|
@ -193,6 +247,9 @@ export default function AppLayout({
|
|||
<main className="p-6">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</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
|
||||
const user = await db.user.findUnique({
|
||||
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(
|
||||
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();
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue