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_ID=${GOOGLE_CLIENT_ID}
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||||
- GOOGLE_REDIRECT_URI=https://store.abrahem.se/api/auth/google/callback
|
- GOOGLE_REDIRECT_URI=https://store.abrahem.se/api/auth/google/callback
|
||||||
|
volumes:
|
||||||
|
- ./public/uploads:/app/public/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||||
import pool from '@/lib/db';
|
import pool from '@/lib/db';
|
||||||
import { verifyToken, TokenPayload } from '@/lib/auth';
|
import { verifyToken, TokenPayload } from '@/lib/auth';
|
||||||
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
import { RowDataPacket, ResultSetHeader } from 'mysql2';
|
||||||
import { sendEmail } from '@/lib/email';
|
import { sendEmail, sendMessageNotification } from '@/lib/email';
|
||||||
|
|
||||||
function getUserFromRequest(request: NextRequest): TokenPayload | null {
|
function getUserFromRequest(request: NextRequest): TokenPayload | null {
|
||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get('authorization');
|
||||||
|
|
@ -112,6 +112,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||||
[id, user.userId, senderRole, content]
|
[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(
|
await pool.query(
|
||||||
`UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
`UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||||
[id]
|
[id]
|
||||||
|
|
@ -129,16 +136,16 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||||
const isCustomerOnline = sessions[0].count > 0;
|
const isCustomerOnline = sessions[0].count > 0;
|
||||||
|
|
||||||
if (!isCustomerOnline) {
|
if (!isCustomerOnline) {
|
||||||
await sendEmail(
|
await sendMessageNotification(
|
||||||
conversation.user_email,
|
conversation.user_email,
|
||||||
`Nytt svar: ${conversation.subject}`,
|
`Nytt svar: ${conversation.subject}`,
|
||||||
`
|
'Nordic Storium Support',
|
||||||
<h2>Du har fått ett svar</h2>
|
process.env.SMTP_FROM || 'support@nordicstorium.com',
|
||||||
<p><strong>Ämne:</strong> ${conversation.subject}</p>
|
null,
|
||||||
<p><strong>Meddelande:</strong></p>
|
null,
|
||||||
<blockquote style="border-left: 3px solid #ccc; padding-left: 10px;">${content}</blockquote>
|
content,
|
||||||
<p><a href="${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/messages/${id}">Se konversation</a></p>
|
`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/messages`,
|
||||||
`
|
'Se konversation'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -157,121 +164,103 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||||
const isAdminOnline = sessions[0].count > 0;
|
const isAdminOnline = sessions[0].count > 0;
|
||||||
|
|
||||||
if (!isAdminOnline) {
|
if (!isAdminOnline) {
|
||||||
await sendEmail(
|
await sendMessageNotification(
|
||||||
admin.email,
|
admin.email,
|
||||||
`Nytt svar: ${conversation.subject}`,
|
`Nytt meddelande: ${conversation.subject}`,
|
||||||
`
|
sender.full_name,
|
||||||
<h2>Kundsvar</h2>
|
sender.email,
|
||||||
<p><strong>Från:</strong> ${conversation.user_name}</p>
|
sender.mobile,
|
||||||
<p><strong>Meddelande:</strong></p>
|
sender.personnummer,
|
||||||
<blockquote style="border-left: 3px solid #ccc; padding-left: 10px;">${content}</blockquote>
|
content,
|
||||||
<p><a href="${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/admin/messages/${id}">Svara här</a></p>
|
if (!user) {
|
||||||
`
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const [convRows] = await pool.query<RowDataPacket[]>(
|
||||||
|
`SELECT * FROM conversations WHERE id = ?`,
|
||||||
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (convRows.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = convRows[0];
|
||||||
|
|
||||||
|
// Only admin can delete completely, customers can only delete their own
|
||||||
|
if (user.role !== 'admin' && conversation.user_id !== user.userId) {
|
||||||
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the conversation (cascades to messages)
|
||||||
|
await pool.query(`DELETE FROM conversations WHERE id = ?`, [id]);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, message: 'Conversation deleted' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting conversation:', error);
|
||||||
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
// PATCH /api/conversations/[id] - Resolve conversation (admin only)
|
||||||
} catch (error) {
|
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||||
console.error('Error sending message:', error);
|
try {
|
||||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
const user = getUserFromRequest(request);
|
||||||
}
|
if (!user) {
|
||||||
}
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE /api/conversations/[id] - Delete conversation
|
if (user.role !== 'admin') {
|
||||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
return NextResponse.json({ error: 'Admin only' }, { status: 403 });
|
||||||
try {
|
}
|
||||||
const user = getUserFromRequest(request);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { action } = body;
|
||||||
|
|
||||||
const [convRows] = await pool.query<RowDataPacket[]>(
|
if (action === 'resolve') {
|
||||||
`SELECT * FROM conversations WHERE id = ?`,
|
// Get conversation info for email
|
||||||
[id]
|
const [convRows] = await pool.query<RowDataPacket[]>(
|
||||||
);
|
`SELECT c.*, u.email as user_email, u.full_name as user_name
|
||||||
|
|
||||||
if (convRows.length === 0) {
|
|
||||||
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversation = convRows[0];
|
|
||||||
|
|
||||||
// Only admin can delete completely, customers can only delete their own
|
|
||||||
if (user.role !== 'admin' && conversation.user_id !== user.userId) {
|
|
||||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the conversation (cascades to messages)
|
|
||||||
await pool.query(`DELETE FROM conversations WHERE id = ?`, [id]);
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true, message: 'Conversation deleted' });
|
|
||||||
} catch (error) {
|
|
||||||
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) {
|
|
||||||
try {
|
|
||||||
const user = getUserFromRequest(request);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role !== 'admin') {
|
|
||||||
return NextResponse.json({ error: 'Admin only' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = await params;
|
|
||||||
const body = await request.json();
|
|
||||||
const { action } = body;
|
|
||||||
|
|
||||||
if (action === 'resolve') {
|
|
||||||
// Get conversation info for email
|
|
||||||
const [convRows] = await pool.query<RowDataPacket[]>(
|
|
||||||
`SELECT c.*, u.email as user_email, u.full_name as user_name
|
|
||||||
FROM conversations c
|
FROM conversations c
|
||||||
JOIN users u ON c.user_id = u.id
|
JOIN users u ON c.user_id = u.id
|
||||||
WHERE c.id = ?`,
|
WHERE c.id = ?`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (convRows.length === 0) {
|
if (convRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = convRows[0];
|
const conversation = convRows[0];
|
||||||
|
|
||||||
// Update status to closed
|
// Update status to closed
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE conversations SET status = 'closed' WHERE id = ?`,
|
`UPDATE conversations SET status = 'closed' WHERE id = ?`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send resolution email to customer
|
// Send resolution email to customer
|
||||||
await sendEmail(
|
await sendEmail(
|
||||||
conversation.user_email,
|
conversation.user_email,
|
||||||
`Ärende löst: ${conversation.subject}`,
|
`Ärende löst: ${conversation.subject}`,
|
||||||
`
|
`
|
||||||
<h2>Ditt ärende har markerats som löst</h2>
|
<h2>Ditt ärende har markerats som löst</h2>
|
||||||
<p>Hej ${conversation.user_name || 'kund'},</p>
|
<p>Hej ${conversation.user_name || 'kund'},</p>
|
||||||
<p>Vi har markerat ditt ärende "<strong>${conversation.subject}</strong>" som löst.</p>
|
<p>Vi har markerat ditt ärende "<strong>${conversation.subject}</strong>" som löst.</p>
|
||||||
<p>Om du fortfarande har frågor eller känner att ärendet inte är helt löst, tveka inte att kontakta oss igen.</p>
|
<p>Om du fortfarande har frågor eller känner att ärendet inte är helt löst, tveka inte att kontakta oss igen.</p>
|
||||||
<p>Med vänliga hälsningar,<br/>Nordic Storium Support</p>
|
<p>Med vänliga hälsningar,<br/>Nordic Storium Support</p>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json({ success: true, message: 'Conversation resolved' });
|
return NextResponse.json({ success: true, message: 'Conversation resolved' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resolving conversation:', error);
|
console.error('Error resolving conversation:', error);
|
||||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
|
import { useNotification } from '@/context/NotificationContext';
|
||||||
import { useToast } from '@/components/Toast';
|
import { useToast } from '@/components/Toast';
|
||||||
import { Loader2, Send, MessageCircle, Plus, Trash2, CheckCircle, AlertTriangle } from 'lucide-react';
|
import { Loader2, Send, MessageCircle, Plus, Trash2, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||||
import '@/styles/Messages.css';
|
import '@/styles/Messages.css';
|
||||||
|
|
@ -35,6 +36,7 @@ export default function MessagesPage() {
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { refreshUnreadCount } = useNotification();
|
||||||
|
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
|
const [activeConversation, setActiveConversation] = useState<Conversation | null>(null);
|
||||||
|
|
@ -102,6 +104,8 @@ export default function MessagesPage() {
|
||||||
setConversations(prev => prev.map(c =>
|
setConversations(prev => prev.map(c =>
|
||||||
c.id === convId ? { ...c, unread_count: 0 } : c
|
c.id === convId ? { ...c, unread_count: 0 } : c
|
||||||
));
|
));
|
||||||
|
// Refresh global navbar count
|
||||||
|
refreshUnreadCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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.
|
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>
|
||||||
<p className="text-xs sm:text-sm border-t border-[var(--border-color)] pt-4 mt-4">
|
<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>.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { User, MessageCircle } from "lucide-react";
|
||||||
import logo from "../assets/logo-main.png";
|
import logo from "../assets/logo-main.png";
|
||||||
import "../styles/components/Navbar.css";
|
import "../styles/components/Navbar.css";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { useNotification } from "../context/NotificationContext";
|
||||||
import NavbarSearch from "./NavbarSearch";
|
import NavbarSearch from "./NavbarSearch";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -42,7 +43,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
<ul className="nav-links desktop-links">
|
<ul className="nav-links desktop-links">
|
||||||
<li className={pathname === "/" ? "current-menu-item" : ""}>
|
<li className={pathname === "/" ? "current-menu-item" : ""}>
|
||||||
<Link href="/" prefetch={true}>HEM</Link>
|
<a href="/">HEM</a>
|
||||||
</li>
|
</li>
|
||||||
<li className={pathname === "/products" ? "current-menu-item" : ""}>
|
<li className={pathname === "/products" ? "current-menu-item" : ""}>
|
||||||
<Link href="/products" prefetch={true}>PRODUKTER</Link>
|
<Link href="/products" prefetch={true}>PRODUKTER</Link>
|
||||||
|
|
@ -61,7 +62,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
{/* Center: Logo Only */}
|
{/* Center: Logo Only */}
|
||||||
<div className="nav-center">
|
<div className="nav-center">
|
||||||
<Link href="/" className="logo-link">
|
<a href="/" className="logo-link">
|
||||||
<Image
|
<Image
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
|
|
@ -70,7 +71,7 @@ export default function Navbar() {
|
||||||
className="logo"
|
className="logo"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side: profile + theme toggle */}
|
{/* Right side: profile + theme toggle */}
|
||||||
|
|
@ -82,7 +83,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
function NavbarClientExtras({ isAuthenticated, isMounted }: { isAuthenticated: boolean; isMounted: boolean }) {
|
function NavbarClientExtras({ isAuthenticated, isMounted }: { isAuthenticated: boolean; isMounted: boolean }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const { unreadCount } = useNotification();
|
||||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||||
const [showThemeToggle, setShowThemeToggle] = useState(true);
|
const [showThemeToggle, setShowThemeToggle] = useState(true);
|
||||||
|
|
||||||
|
|
@ -124,30 +125,7 @@ function NavbarClientExtras({ isAuthenticated, isMounted }: { isAuthenticated: b
|
||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, [theme]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const handleTrigger = () => {
|
const handleTrigger = () => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ThemeProvider } from "./theme-provider";
|
import { ThemeProvider } from "./theme-provider";
|
||||||
import { AuthProvider } from "../context/AuthContext";
|
import { AuthProvider } from "../context/AuthContext";
|
||||||
|
import { NotificationProvider } from "../context/NotificationContext";
|
||||||
import { ToastProvider } from "./Toast";
|
import { ToastProvider } from "./Toast";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
|
@ -17,9 +18,11 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ToastProvider>
|
<NotificationProvider>
|
||||||
{children}
|
<ToastProvider>
|
||||||
</ToastProvider>
|
{children}
|
||||||
|
</ToastProvider>
|
||||||
|
</NotificationProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</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;
|
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