footer+responsivenes

This commit is contained in:
Timo 2026-01-01 20:18:45 +01:00
parent 7afc865a3f
commit 91313ac7d5
6 changed files with 197 additions and 3 deletions

View File

@ -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

View File

@ -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>
);

View File

@ -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() },

39
src/app/api/user/route.ts Normal file
View File

@ -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 }
);
}
}

View File

@ -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>&copy; 2025 QR Master. All rights reserved.</p>
<div className="w-12"></div>
</div>
</div>
</footer>
);
}

View File

@ -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();
},
{