fix uploads
fix refresh page on navbar hem/logo fix messages icon notification
This commit is contained in:
parent
0dbddac214
commit
a17c82aa30
|
|
@ -40,6 +40,8 @@ services:
|
|||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GOOGLE_REDIRECT_URI=https://store.abrahem.se/api/auth/google/callback
|
||||
volumes:
|
||||
- ./public/uploads:/app/public/uploads
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||
import pool from '@/lib/db';
|
||||
import { verifyToken, TokenPayload } from '@/lib/auth';
|
||||
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||
import { sendEmail } from '@/lib/email';
|
||||
import { sendEmail, sendMessageNotification } from '@/lib/email';
|
||||
|
||||
function getUserFromRequest(request: NextRequest): TokenPayload | null {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
|
@ -112,6 +112,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||
[id, user.userId, senderRole, content]
|
||||
);
|
||||
|
||||
// Fetch sender's expanded details for the email
|
||||
const [senderDetails] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT full_name, email, mobile, personnummer FROM users WHERE id = ?`,
|
||||
[user.userId]
|
||||
);
|
||||
const sender = senderDetails[0];
|
||||
|
||||
await pool.query(
|
||||
`UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||
[id]
|
||||
|
|
@ -129,16 +136,16 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||
const isCustomerOnline = sessions[0].count > 0;
|
||||
|
||||
if (!isCustomerOnline) {
|
||||
await sendEmail(
|
||||
await sendMessageNotification(
|
||||
conversation.user_email,
|
||||
`Nytt svar: ${conversation.subject}`,
|
||||
`
|
||||
<h2>Du har fått ett svar</h2>
|
||||
<p><strong>Ämne:</strong> ${conversation.subject}</p>
|
||||
<p><strong>Meddelande:</strong></p>
|
||||
<blockquote style="border-left: 3px solid #ccc; padding-left: 10px;">${content}</blockquote>
|
||||
<p><a href="${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/messages/${id}">Se konversation</a></p>
|
||||
`
|
||||
'Nordic Storium Support',
|
||||
process.env.SMTP_FROM || 'support@nordicstorium.com',
|
||||
null,
|
||||
null,
|
||||
content,
|
||||
`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/messages`,
|
||||
'Se konversation'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -157,32 +164,14 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|||
const isAdminOnline = sessions[0].count > 0;
|
||||
|
||||
if (!isAdminOnline) {
|
||||
await sendEmail(
|
||||
await sendMessageNotification(
|
||||
admin.email,
|
||||
`Nytt svar: ${conversation.subject}`,
|
||||
`
|
||||
<h2>Kundsvar</h2>
|
||||
<p><strong>Från:</strong> ${conversation.user_name}</p>
|
||||
<p><strong>Meddelande:</strong></p>
|
||||
<blockquote style="border-left: 3px solid #ccc; padding-left: 10px;">${content}</blockquote>
|
||||
<p><a href="${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/admin/messages/${id}">Svara här</a></p>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/conversations/[id] - Delete conversation
|
||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const user = getUserFromRequest(request);
|
||||
`Nytt meddelande: ${conversation.subject}`,
|
||||
sender.full_name,
|
||||
sender.email,
|
||||
sender.mobile,
|
||||
sender.personnummer,
|
||||
content,
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
|
@ -213,10 +202,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|||
console.error('Error deleting conversation:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/conversations/[id] - Resolve conversation (admin only)
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
// PATCH /api/conversations/[id] - Resolve conversation (admin only)
|
||||
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const user = getUserFromRequest(request);
|
||||
if (!user) {
|
||||
|
|
@ -274,4 +263,4 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|||
console.error('Error resolving conversation:', error);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useNotification } from '@/context/NotificationContext';
|
||||
import { useToast } from '@/components/Toast';
|
||||
import { Loader2, Send, MessageCircle, Plus, Trash2, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||
import '@/styles/Messages.css';
|
||||
|
|
@ -35,6 +36,7 @@ export default function MessagesPage() {
|
|||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const { refreshUnreadCount } = useNotification();
|
||||
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
|
||||
|
|
@ -102,6 +104,8 @@ export default function MessagesPage() {
|
|||
setConversations(prev => prev.map(c =>
|
||||
c.id === convId ? { ...c, unread_count: 0 } : c
|
||||
));
|
||||
// Refresh global navbar count
|
||||
refreshUnreadCount();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export default function CookieConsentModal() {
|
|||
Vi värdesätter transparens. Genom att klicka på "Godkänn alla" samtycker du till användningen av alla cookies. Du kan när som helst ändra dina val eller återkalla ditt samtycke under "Anpassa" eller via länken i sidfoten.
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm border-t border-[var(--border-color)] pt-4 mt-4">
|
||||
Personuppgiftsansvarig: <strong>Nordic Storium</strong> (Org.nr 559287-3612). <br />
|
||||
Personuppgiftsansvarig: <strong>Nordic Storium</strong><br />
|
||||
Läs mer om dina rättigheter i vår <Link href="/integritetspolicy" className="text-blue-600 hover:underline font-medium">Integritetspolicy</Link> och <Link href="/villkor" className="text-blue-600 hover:underline font-medium">Köpvillkor</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { User, MessageCircle } from "lucide-react";
|
|||
import logo from "../assets/logo-main.png";
|
||||
import "../styles/components/Navbar.css";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useNotification } from "../context/NotificationContext";
|
||||
import NavbarSearch from "./NavbarSearch";
|
||||
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ export default function Navbar() {
|
|||
|
||||
<ul className="nav-links desktop-links">
|
||||
<li className={pathname === "/" ? "current-menu-item" : ""}>
|
||||
<Link href="/" prefetch={true}>HEM</Link>
|
||||
<a href="/">HEM</a>
|
||||
</li>
|
||||
<li className={pathname === "/products" ? "current-menu-item" : ""}>
|
||||
<Link href="/products" prefetch={true}>PRODUKTER</Link>
|
||||
|
|
@ -61,7 +62,7 @@ export default function Navbar() {
|
|||
|
||||
{/* Center: Logo Only */}
|
||||
<div className="nav-center">
|
||||
<Link href="/" className="logo-link">
|
||||
<a href="/" className="logo-link">
|
||||
<Image
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
|
|
@ -70,7 +71,7 @@ export default function Navbar() {
|
|||
className="logo"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Right side: profile + theme toggle */}
|
||||
|
|
@ -82,7 +83,7 @@ export default function Navbar() {
|
|||
|
||||
function NavbarClientExtras({ isAuthenticated, isMounted }: { isAuthenticated: boolean; isMounted: boolean }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const { unreadCount } = useNotification();
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [showThemeToggle, setShowThemeToggle] = useState(true);
|
||||
|
||||
|
|
@ -124,30 +125,7 @@ function NavbarClientExtras({ isAuthenticated, isMounted }: { isAuthenticated: b
|
|||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [theme]);
|
||||
|
||||
// Fetch unread message count
|
||||
useEffect(() => {
|
||||
if (!isMounted || !isAuthenticated) return;
|
||||
|
||||
const fetchUnread = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
const res = await fetch('/api/conversations/unread', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUnreadCount(data.count);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUnread();
|
||||
// Poll every 30 seconds
|
||||
const interval = setInterval(fetchUnread, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isMounted, isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTrigger = () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import React from "react";
|
||||
import { ThemeProvider } from "./theme-provider";
|
||||
import { AuthProvider } from "../context/AuthContext";
|
||||
import { NotificationProvider } from "../context/NotificationContext";
|
||||
import { ToastProvider } from "./Toast";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
|
|
@ -17,9 +18,11 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||
disableTransitionOnChange
|
||||
>
|
||||
<AuthProvider>
|
||||
<NotificationProvider>
|
||||
<ToastProvider>
|
||||
{children}
|
||||
</ToastProvider>
|
||||
</NotificationProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
|
||||
import { useAuth } from "./AuthContext";
|
||||
|
||||
interface NotificationContextType {
|
||||
unreadCount: number;
|
||||
refreshUnreadCount: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
|
||||
|
||||
export function NotificationProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const fetchUnread = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
setUnreadCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const res = await fetch('/api/conversations/unread', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUnreadCount(data.count);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch unread count:", err);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Initial fetch and poller
|
||||
useEffect(() => {
|
||||
if (!isMounted || !isAuthenticated) return;
|
||||
|
||||
fetchUnread();
|
||||
|
||||
// Poll every 30 seconds
|
||||
const interval = setInterval(fetchUnread, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isMounted, isAuthenticated, fetchUnread]);
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{ unreadCount, refreshUnreadCount: fetchUnread }}>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNotification() {
|
||||
const context = useContext(NotificationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useNotification must be used within a NotificationProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -161,3 +161,43 @@ export const sendSellFurnitureEmail = async (
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMessageNotification = async (
|
||||
toEmail: string,
|
||||
subject: string,
|
||||
senderName: string,
|
||||
senderEmail: string,
|
||||
senderPhone: string | null,
|
||||
senderPersonnummer: string | null,
|
||||
messageContent: string,
|
||||
actionLink: string,
|
||||
actionText: string
|
||||
) => {
|
||||
const content = `
|
||||
<h2 style="font-size: 16px; font-weight: 800; text-transform: uppercase; margin-bottom: 20px; letter-spacing: 1px;">Nytt Meddelande</h2>
|
||||
<div style="background: #f7fafc; padding: 20px; border-left: 4px solid #000000; margin-bottom: 30px;">
|
||||
<p style="font-size: 14px; margin: 0 0 10px 0;"><strong>Från:</strong> ${senderName}</p>
|
||||
<p style="font-size: 14px; margin: 0 0 10px 0;"><strong>Email:</strong> <a href="mailto:${senderEmail}" style="color: #000000;">${senderEmail}</a></p>
|
||||
${senderPhone ? `<p style="font-size: 14px; margin: 0 0 10px 0;"><strong>Telefon:</strong> ${senderPhone}</p>` : ''}
|
||||
${senderPersonnummer ? `<p style="font-size: 14px; margin: 0;"><strong>Personnummer:</strong> ${senderPersonnummer}</p>` : ''}
|
||||
</div>
|
||||
|
||||
<div style="background: #ffffff; padding: 20px; border: 1px solid #e2e8f0; margin-bottom: 30px;">
|
||||
<h3 style="font-size: 14px; font-weight: 700; text-transform: uppercase; margin: 0 0 15px 0; letter-spacing: 1px;">Meddelande:</h3>
|
||||
<blockquote style="font-size: 14px; line-height: 1.7; color: #444444; margin: 0; padding-left: 10px; border-left: 3px solid #e2e8f0;">
|
||||
${messageContent}
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<a href="${actionLink}" style="display: block; background: #000000; color: #ffffff; text-align: center; padding: 16px; text-decoration: none; font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: 1px; border-radius: 4px;">
|
||||
${actionText}
|
||||
</a>
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e2e8f0;">
|
||||
<p style="font-size: 12px; color: #999999;">
|
||||
Klicka på knappen ovan för att svara direkt i portalen.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
return sendEmail(toEmail, subject, premiumTemplate(content));
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue