initian commit
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
*.log
|
||||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# ---------- deps ----------
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
# helps certain native packages (e.g., sharp) on Alpine
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
|
||||||
|
# install with your lockfile manager
|
||||||
|
RUN if [ -f pnpm-lock.yaml ]; then npm i -g pnpm@8 && pnpm i --frozen-lockfile; \
|
||||||
|
elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
|
||||||
|
else npm ci; fi
|
||||||
|
|
||||||
|
# ---------- builder ----------
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------- runner ----------
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
# non-root user
|
||||||
|
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D nextjs
|
||||||
|
|
||||||
|
# copy the minimal standalone output
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
next:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: my-next:latest
|
||||||
|
container_name: nextjs
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- TZ=Europe/Ljubljana
|
||||||
|
# add any Next.js runtime env vars you need, e.g.:
|
||||||
|
# - NEXTAUTH_URL=https://example.com
|
||||||
|
# - NEXT_PUBLIC_API_BASE=https://api.example.com
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# tell Traefik which network to use to reach the container
|
||||||
|
- "traefik.docker.network=proxy"
|
||||||
|
|
||||||
|
# Router (replace example.com)
|
||||||
|
- "traefik.http.routers.next.rule=Host(`${DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.next.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.next.tls=true"
|
||||||
|
# use your existing certresolver name from your Traefik config
|
||||||
|
- "traefik.http.routers.next.tls.certresolver=letsencrypt"
|
||||||
|
|
||||||
|
# Service (container listens on 3000)
|
||||||
|
- "traefik.http.services.next.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
@@ -2,6 +2,8 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
output: 'standalone',
|
||||||
|
telemetry: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
1944
package-lock.json
generated
1944
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,22 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "petadvisor",
|
"name": "petadviser",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/react": "^3.24.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@supabase/ssr": "^0.6.1",
|
||||||
|
"@supabase/supabase-js": "^2.53.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"leaflet.markercluster": "^1.5.3",
|
||||||
|
"next": "15.4.5",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.4.5"
|
"react-icons": "^5.5.0",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-leaflet-markercluster": "^5.0.0-rc.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19"
|
"@types/react-dom": "^19",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,46 @@
|
|||||||
:root {
|
@import "leaflet/dist/leaflet.css";
|
||||||
--background: #ffffff;
|
@import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
--foreground: #171717;
|
@import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/*:root {*/
|
||||||
:root {
|
/* --background: #ffffff;*/
|
||||||
--background: #0a0a0a;
|
/* --foreground: #171717;*/
|
||||||
--foreground: #ededed;
|
/*}*/
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
/*@media (prefers-color-scheme: dark) {*/
|
||||||
body {
|
/* :root {*/
|
||||||
max-width: 100vw;
|
/* --background: #0a0a0a;*/
|
||||||
overflow-x: hidden;
|
/* --foreground: #ededed;*/
|
||||||
}
|
/* }*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
body {
|
/*html,*/
|
||||||
color: var(--foreground);
|
/*body {*/
|
||||||
background: var(--background);
|
/* max-width: 100vw;*/
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
/* overflow-x: hidden;*/
|
||||||
-webkit-font-smoothing: antialiased;
|
/*}*/
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
/*body {*/
|
||||||
box-sizing: border-box;
|
/* color: var(--foreground);*/
|
||||||
padding: 0;
|
/* background: var(--background);*/
|
||||||
margin: 0;
|
/* font-family: Arial, Helvetica, sans-serif;*/
|
||||||
}
|
/* -webkit-font-smoothing: antialiased;*/
|
||||||
|
/* -moz-osx-font-smoothing: grayscale;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
a {
|
/** {*/
|
||||||
color: inherit;
|
/* box-sizing: border-box;*/
|
||||||
text-decoration: none;
|
/* padding: 0;*/
|
||||||
}
|
/* margin: 0;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/*a {*/
|
||||||
html {
|
/* color: inherit;*/
|
||||||
color-scheme: dark;
|
/* text-decoration: none;*/
|
||||||
}
|
/*}*/
|
||||||
}
|
|
||||||
|
/*@media (prefers-color-scheme: dark) {*/
|
||||||
|
/* html {*/
|
||||||
|
/* color-scheme: dark;*/
|
||||||
|
/* }*/
|
||||||
|
/*}*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type {Metadata} from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import {Geist, Geist_Mono} from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import {Provider} from "@/components/ui/provider"
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -13,19 +14,19 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Pet Adviser",
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html suppressHydrationWarning lang="en">
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||||
{children}
|
<Provider>{children}</Provider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,167 +1,3 @@
|
|||||||
.page {
|
|
||||||
--gray-rgb: 0, 0, 0;
|
|
||||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
|
||||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
|
||||||
|
|
||||||
--button-primary-hover: #383838;
|
|
||||||
--button-secondary-hover: #f2f2f2;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 20px 1fr 20px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
min-height: 100svh;
|
|
||||||
padding: 80px;
|
|
||||||
gap: 64px;
|
|
||||||
font-family: var(--font-geist-sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.page {
|
|
||||||
--gray-rgb: 255, 255, 255;
|
|
||||||
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
|
||||||
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
|
||||||
|
|
||||||
--button-primary-hover: #ccc;
|
|
||||||
--button-secondary-hover: #1a1a1a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 32px;
|
|
||||||
grid-row-start: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main ol {
|
|
||||||
font-family: var(--font-geist-mono);
|
|
||||||
padding-left: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 24px;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main li:not(:last-of-type) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main code {
|
|
||||||
font-family: inherit;
|
|
||||||
background: var(--gray-alpha-100);
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas a {
|
|
||||||
appearance: none;
|
|
||||||
border-radius: 128px;
|
|
||||||
height: 48px;
|
|
||||||
padding: 0 20px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
transition:
|
|
||||||
background 0.2s,
|
|
||||||
color 0.2s,
|
|
||||||
border-color 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.primary {
|
|
||||||
background: var(--foreground);
|
|
||||||
color: var(--background);
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary {
|
|
||||||
border-color: var(--gray-alpha-200);
|
|
||||||
min-width: 158px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
grid-row-start: 3;
|
background-color: #f6f6f6;
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer img {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enable hover only on non-touch devices */
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
|
||||||
a.primary:hover {
|
|
||||||
background: var(--button-primary-hover);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary:hover {
|
|
||||||
background: var(--button-secondary-hover);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.page {
|
|
||||||
padding: 32px;
|
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main ol {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ctas a {
|
|
||||||
font-size: 14px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.secondary {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.logo {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
109
src/app/page.tsx
109
src/app/page.tsx
@@ -1,94 +1,39 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styles from "./page.module.css";
|
import styles from "./page.module.css";
|
||||||
|
import { Container, Heading } from "@chakra-ui/react"
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import HeroBanner from "@/components/HeroBanner";
|
||||||
|
import CompaniesExplorer from "@/components/Map/CompaniesExplorer.tsx";
|
||||||
|
import type { Company } from "@/components/Map/CompanyMap.tsx";
|
||||||
|
import Articles from "@/components/Articles";
|
||||||
|
import Calendar from "@/components/Calendar";
|
||||||
|
import Sponsor from "@/components/Sponsor";
|
||||||
|
import Footer from "@/components/Footer";
|
||||||
|
|
||||||
|
const seed: Company[] = [
|
||||||
|
{ id: "1", name: "ByteForge", industry: "Tech", lat: 46.0569, lng: 14.5058, address: "Ljubljana" },
|
||||||
|
{ id: "2", name: "GreenPlate", industry: "Food", lat: 46.2389, lng: 15.2675, address: "Celje" },
|
||||||
|
{ id: "3", name: "Medicus+", industry: "Healthcare", lat: 46.5547, lng: 15.6459, address: "Maribor" },
|
||||||
|
{ id: "4", name: "ShopNook", industry: "Retail", lat: 45.5481, lng: 13.7302, address: "Koper" },
|
||||||
|
{ id: "5", name: "Steelworks d.o.o.", industry: "Manufacturing", lat: 46.2396, lng: 14.3556, address: "Kranj" },
|
||||||
|
{ id: "6", name: "CloudLynx", industry: "Tech", lat: 46.3607, lng: 14.0888, address: "Škofja Loka" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
|
<header>
|
||||||
|
<Header/>
|
||||||
|
</header>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Image
|
<HeroBanner/>
|
||||||
className={styles.logo}
|
<CompaniesExplorer companies={seed} />
|
||||||
src="/next.svg"
|
<Articles />
|
||||||
alt="Next.js logo"
|
<Sponsor />
|
||||||
width={180}
|
<Calendar initialDate={new Date()} />
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
Get started by editing <code>src/app/page.tsx</code>.
|
|
||||||
</li>
|
|
||||||
<li>Save and see your changes instantly.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className={styles.ctas}>
|
|
||||||
<a
|
|
||||||
className={styles.primary}
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className={styles.logo}
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className={styles.secondary}
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<a
|
<Footer />
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
35
src/components/Articles.js
Normal file
35
src/components/Articles.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createClient } from '@/utils/supabase/server';
|
||||||
|
import { Container, Button, Card, Heading, Image, Text, SimpleGrid, GridItem } from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export default async function Articles() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const { data: articles } = await supabase.from("articles").select().order("published_at", {ascending: false}).limit(8);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Heading size="4xl" as="h2">Zadnje novice iz sveta malih živali</Heading>
|
||||||
|
{articles.length === 0 ? <Text>Ni vsebin</Text> : <SimpleGrid columns={4} gap="40px">
|
||||||
|
{articles.map((article) => (
|
||||||
|
<Card.Root key={article.id} maxW="sm" overflow="hidden">
|
||||||
|
<Image
|
||||||
|
src={article.image_url}
|
||||||
|
alt="Green double couch with wooden legs"
|
||||||
|
/>
|
||||||
|
<Card.Body gap="2">
|
||||||
|
<Card.Title>{article.title}</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
{article.name}
|
||||||
|
</Card.Description>
|
||||||
|
<small className="text-gray-500">
|
||||||
|
{new Date(article.published_at).toLocaleString("sl-SI", {weekday: "long", year: "numeric", month: "long", day: "numeric"})}
|
||||||
|
</small>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer gap="2">
|
||||||
|
<Button variant="outline">Ogled objave</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
381
src/components/Calendar.js
Normal file
381
src/components/Calendar.js
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
addMonths,
|
||||||
|
subMonths,
|
||||||
|
startOfMonth,
|
||||||
|
endOfMonth,
|
||||||
|
startOfWeek,
|
||||||
|
endOfWeek,
|
||||||
|
addDays,
|
||||||
|
isSameMonth,
|
||||||
|
isSameDay,
|
||||||
|
format,
|
||||||
|
parseISO,
|
||||||
|
isAfter,
|
||||||
|
} from "date-fns";
|
||||||
|
import { FaChevronLeft, FaChevronRight, FaCalendarPlus, FaClock, FaMapMarkerAlt, FaRegCalendarAlt } from "react-icons/fa";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Badge,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Input,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
// --- Demo Data ---
|
||||||
|
const demoEvents = [
|
||||||
|
{ id: "1", title: "Dogodek 1", start: new Date().toISOString(), color: "blue.500" },
|
||||||
|
{ id: "2", title: "Dogodek 2", start: addDays(new Date(), 2).toISOString(), color: "pink.500" },
|
||||||
|
{ id: "3", title: "Dogodek 3", start: addDays(new Date(), 6).toISOString(), color: "green.500" },
|
||||||
|
{ id: "4", title: "Dogodek 4", start: addDays(new Date(), 12).toISOString(), color: "orange.500" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
const getMonthMatrix = (current) => {
|
||||||
|
const monthStart = startOfMonth(current);
|
||||||
|
const monthEnd = endOfMonth(current);
|
||||||
|
const startDate = startOfWeek(monthStart, { weekStartsOn: 1 }); // Monday
|
||||||
|
const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
let day = startDate;
|
||||||
|
let row = [];
|
||||||
|
|
||||||
|
while (day <= endDate) {
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
row.push(day);
|
||||||
|
day = addDays(day, 1);
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
row = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DayBadge = ({ count }) =>
|
||||||
|
count > 0 ? (
|
||||||
|
<Badge ml={1} fontSize="0.6rem" colorScheme="purple" variant="subtle" borderRadius="full" px={2} py={0.5}>
|
||||||
|
{count}
|
||||||
|
</Badge>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const EventPill = ({ ev }) => (
|
||||||
|
<HStack spacing={2} px={2} py={1} borderRadius="xl" bg={ev.color ?? "gray.500"} color="white" _hover={{ opacity: 0.9 }}>
|
||||||
|
<Text fontSize="xs" noOfLines={1} fontWeight="medium">
|
||||||
|
{ev.title}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Main Component (Plain JS, Chakra v1/v2 compatible) ---
|
||||||
|
export default function Calendar({ initialDate, initialEvents = demoEvents }) {
|
||||||
|
// Static tokens (works without Chakra hooks)
|
||||||
|
const mutedColor = "gray.600";
|
||||||
|
const tileBg = "white";
|
||||||
|
const tileBgMuted = "gray.50";
|
||||||
|
const selectedRing = "purple.400";
|
||||||
|
|
||||||
|
const [currentDate, setCurrentDate] = useState(
|
||||||
|
initialDate ? (typeof initialDate === "string" ? parseISO(initialDate) : initialDate) : new Date()
|
||||||
|
);
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [events, setEvents] = useState(initialEvents);
|
||||||
|
|
||||||
|
const monthMatrix = useMemo(() => getMonthMatrix(currentDate), [currentDate]);
|
||||||
|
|
||||||
|
const eventsByDay = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
events.forEach((ev) => {
|
||||||
|
const d = format(parseISO(ev.start), "yyyy-MM-dd");
|
||||||
|
if (!map.has(d)) map.set(d, []);
|
||||||
|
map.get(d).push(ev);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
const todayKey = format(new Date(), "yyyy-MM-dd");
|
||||||
|
|
||||||
|
const upcomingEvents = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
return [...events]
|
||||||
|
.filter((e) => isAfter(parseISO(e.start), addDays(now, -1)))
|
||||||
|
.sort((a, b) => parseISO(a.start).getTime() - parseISO(b.start).getTime());
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
// Quick add form state
|
||||||
|
const [newTitle, setNewTitle] = useState("");
|
||||||
|
const [newDate, setNewDate] = useState(format(new Date(), "yyyy-MM-dd"));
|
||||||
|
const [newTime, setNewTime] = useState("09:00");
|
||||||
|
const [newLocation, setNewLocation] = useState("");
|
||||||
|
|
||||||
|
const addEvent = () => {
|
||||||
|
if (!newTitle.trim()) return;
|
||||||
|
const dt = new Date(`${newDate}T${newTime}:00`);
|
||||||
|
setEvents((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now()),
|
||||||
|
title: newTitle.trim(),
|
||||||
|
start: dt.toISOString(),
|
||||||
|
location: newLocation.trim() || undefined,
|
||||||
|
color: "purple.500",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setNewTitle("");
|
||||||
|
setNewLocation("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Box maxW="6xl" mx="auto" p={{base: 4, md: 8}}>
|
||||||
|
<Flex mb={6} direction={{base: "column", sm: "row"}} gap={3} align={{sm: "center"}}
|
||||||
|
justify="space-between">
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Heading size="4xl" as="h2">
|
||||||
|
Prihajajoči dogodki
|
||||||
|
</Heading>
|
||||||
|
<FaRegCalendarAlt size={20}/>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())}>
|
||||||
|
Danes
|
||||||
|
</Button>
|
||||||
|
<HStack spacing={1} borderWidth="1px" borderRadius="2xl" p={1}>
|
||||||
|
<IconButton aria-label="Prejšnji mesec" variant="ghost" size="sm"
|
||||||
|
onClick={() => setCurrentDate((d) => subMonths(d, 1))}>
|
||||||
|
<FaChevronLeft/>
|
||||||
|
</IconButton>
|
||||||
|
<Text px={3} fontSize="sm" fontWeight="medium" minW="140px" textAlign="center">
|
||||||
|
{format(currentDate, "MMMM yyyy")}
|
||||||
|
</Text>
|
||||||
|
<IconButton aria-label="Naslednji mesec" variant="ghost" size="sm"
|
||||||
|
onClick={() => setCurrentDate((d) => addMonths(d, 1))}>
|
||||||
|
<FaChevronRight/>
|
||||||
|
</IconButton>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Grid templateColumns={{base: "1fr", lg: "2fr 1fr"}} gap={6}>
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<Box borderWidth="1px" borderRadius="2xl" overflow="hidden">
|
||||||
|
<Box pb={0} borderBottomWidth="1px" px={4}>
|
||||||
|
<Grid templateColumns="repeat(7, 1fr)" color={mutedColor} fontSize="xs" fontWeight="medium">
|
||||||
|
{["Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"].map((d) => (
|
||||||
|
<GridItem key={d} px={2} py={3} textAlign="center">
|
||||||
|
{d}
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Box pt={2} px={4}>
|
||||||
|
<Grid templateColumns="repeat(7, 1fr)" gap={1}>
|
||||||
|
{monthMatrix.flat().map((day, idx) => {
|
||||||
|
const inMonth = isSameMonth(day, currentDate);
|
||||||
|
const key = format(day, "yyyy-MM-dd");
|
||||||
|
const isToday = key === todayKey;
|
||||||
|
const isSelected = selectedDate && isSameDay(day, selectedDate);
|
||||||
|
const dayEvents = eventsByDay.get(key) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setSelectedDate(day)}
|
||||||
|
h="96px"
|
||||||
|
p={2}
|
||||||
|
borderRadius="2xl"
|
||||||
|
bg={inMonth ? tileBg : tileBgMuted}
|
||||||
|
color={inMonth ? undefined : mutedColor}
|
||||||
|
borderWidth={isSelected ? "2px" : "0px"}
|
||||||
|
borderColor={isSelected ? selectedRing : "transparent"}
|
||||||
|
>
|
||||||
|
<Flex align="center">
|
||||||
|
<HStack>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
h={6}
|
||||||
|
w={6}
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="semibold"
|
||||||
|
bg={isToday ? "purple.500" : "transparent"}
|
||||||
|
color={isToday ? "white" : undefined}
|
||||||
|
>
|
||||||
|
{format(day, "d")}
|
||||||
|
</Flex>
|
||||||
|
<DayBadge count={dayEvents.length}/>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<VStack mt={2} align="stretch" spacing={1} maxH="56px" overflow="hidden">
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{dayEvents.slice(0, 3).map((ev) => (
|
||||||
|
<motion.div key={ev.id} initial={{opacity: 0, y: -6}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
exit={{opacity: 0, y: 6}}>
|
||||||
|
<EventPill ev={ev}/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
{dayEvents.length > 3 && (
|
||||||
|
<Text fontSize="xs" color={mutedColor} noOfLines={1}>
|
||||||
|
+{dayEvents.length - 3} more
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Selected Day Drawer */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedDate && (
|
||||||
|
<motion.div initial={{opacity: 0, y: 10}} animate={{opacity: 1, y: 0}}
|
||||||
|
exit={{opacity: 0, y: 10}}>
|
||||||
|
<Box mt={4} borderWidth="1px" borderRadius="2xl" p={4}>
|
||||||
|
<Text fontSize="sm" color={mutedColor}>
|
||||||
|
Na izbran dan
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="md" fontWeight="medium">
|
||||||
|
{format(selectedDate, "EEEE, MMM d")}
|
||||||
|
</Text>
|
||||||
|
<VStack align="stretch" spacing={2} mt={2}>
|
||||||
|
{(eventsByDay.get(format(selectedDate, "yyyy-MM-dd")) || []).length === 0 ? (
|
||||||
|
<Text fontSize="sm" color={mutedColor}>
|
||||||
|
No events for this day.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
(eventsByDay.get(format(selectedDate, "yyyy-MM-dd")) || []).map((ev) => (
|
||||||
|
<HStack key={ev.id} align="start" spacing={3} borderWidth="1px"
|
||||||
|
borderRadius="xl" p={3}>
|
||||||
|
<Box mt={0.5} h={2.5} w={2.5} borderRadius="full"
|
||||||
|
bg={ev.color ?? "gray.400"}/>
|
||||||
|
<Box flex="1">
|
||||||
|
<Text fontWeight="medium" noOfLines={2}>
|
||||||
|
{ev.title}
|
||||||
|
</Text>
|
||||||
|
<HStack mt={1} spacing={3} wrap="wrap"
|
||||||
|
color={mutedColor} fontSize="xs">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FaClock/>
|
||||||
|
<Text>{format(parseISO(ev.start), "EEE, MMM d HH:mm")}</Text>
|
||||||
|
</HStack>
|
||||||
|
{ev.location && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FaMapMarkerAlt/>
|
||||||
|
<Text>{ev.location}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Sidebar */}
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* Quick add
|
||||||
|
<Box borderWidth="1px" borderRadius="2xl">
|
||||||
|
<Box pb={0} borderBottomWidth="1px" px={4} py={3}>
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
Quick add event
|
||||||
|
</Heading>
|
||||||
|
</Box>
|
||||||
|
<Box pt={4} px={4} pb={4}>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
<Box>
|
||||||
|
<Text as="label" htmlFor="title" fontSize="sm" fontWeight="medium" mb={1} display="block">Title</Text>
|
||||||
|
<Input id="title" value={newTitle} onChange={(e) => setNewTitle(e.target.value)} />
|
||||||
|
</Box>
|
||||||
|
<Grid templateColumns="repeat(2, 1fr)" gap={3}>
|
||||||
|
<Box>
|
||||||
|
<Text as="label" htmlFor="date" fontSize="sm" fontWeight="medium" mb={1} display="block">Date</Text>
|
||||||
|
<Input id="date" type="date" value={newDate} onChange={(e) => setNewDate(e.target.value)} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text as="label" htmlFor="time" fontSize="sm" fontWeight="medium" mb={1} display="block">Time</Text>
|
||||||
|
<Input id="time" type="time" value={newTime} onChange={(e) => setNewTime(e.target.value)} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Box>
|
||||||
|
<Text as="label" htmlFor="location" fontSize="sm" fontWeight="medium" mb={1} display="block">Location</Text>
|
||||||
|
<Input id="location" value={newLocation} onChange={(e) => setNewLocation(e.target.value)} />
|
||||||
|
</Box>
|
||||||
|
<Button onClick={addEvent} leftIcon={<FaCalendarPlus />} colorScheme="purple" alignSelf="flex-start">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</Box>*/}
|
||||||
|
|
||||||
|
{/* Upcoming */}
|
||||||
|
<Box h="520px" borderWidth="1px" borderRadius="2xl" overflow="hidden">
|
||||||
|
<Box pb={0} borderBottomWidth="1px" px={4} py={3}>
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
Prihajajoči dogodki
|
||||||
|
</Heading>
|
||||||
|
</Box>
|
||||||
|
<Box pt={3} px={4} h="full" overflowY="auto" pr={3} pb={4}>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{upcomingEvents.length === 0 ? (
|
||||||
|
<Text fontSize="sm" color={mutedColor}>
|
||||||
|
Trenutno prihodnjih dogodkov.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
upcomingEvents.map((ev) => (
|
||||||
|
<HStack key={ev.id} align="start" spacing={3} borderWidth="1px"
|
||||||
|
borderRadius="xl" p={3}>
|
||||||
|
<Box mt={0.5} h={2.5} w={2.5} borderRadius="full"
|
||||||
|
bg={ev.color ?? "gray.400"}/>
|
||||||
|
<Box flex="1" minW={0}>
|
||||||
|
<Text fontWeight="medium" noOfLines={1}>
|
||||||
|
{ev.title}
|
||||||
|
</Text>
|
||||||
|
<HStack mt={1} spacing={3} wrap="wrap" color={mutedColor}
|
||||||
|
fontSize="xs">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FaClock/>
|
||||||
|
<Text>{format(parseISO(ev.start), "EEE, MMM d HH:mm")}</Text>
|
||||||
|
</HStack>
|
||||||
|
{ev.location && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<FaMapMarkerAlt/>
|
||||||
|
<Text>{ev.location}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/Footer.js
Normal file
43
src/components/Footer.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {Container, Grid, List, Heading, Image, IconButton, Stack} from "@chakra-ui/react";
|
||||||
|
import { LuPhone, LuMail } from "react-icons/lu"
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Grid templateColumns="repeat(3, 1fr)" gap="6">
|
||||||
|
<div>
|
||||||
|
<Heading as="h3">Uporabne povezave</Heading>
|
||||||
|
<List.Root>
|
||||||
|
<List.Item>Povezava 1</List.Item>
|
||||||
|
<List.Item>Povezava 2</List.Item>
|
||||||
|
<List.Item>Povezava 3</List.Item>
|
||||||
|
<List.Item>Povezava 4</List.Item>
|
||||||
|
</List.Root>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Heading as="h3">PetAdviser</Heading>
|
||||||
|
<List.Root>
|
||||||
|
<List.Item>O nas 1</List.Item>
|
||||||
|
<List.Item>O nas 2</List.Item>
|
||||||
|
<List.Item>O nas 3</List.Item>
|
||||||
|
<List.Item>O nas 4</List.Item>
|
||||||
|
</List.Root>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Heading as="h3">Vizitka</Heading>
|
||||||
|
<Image src="/logo.svg" alt="Pet Adviser Logo" h="40px" />
|
||||||
|
<Stack direction={{ base: "column", md: "column" }} gap="4">
|
||||||
|
<IconButton variant="outline" aria-label="E-posta" rounded="full">
|
||||||
|
<LuMail />
|
||||||
|
info@petadviser.si
|
||||||
|
</IconButton>
|
||||||
|
<IconButton variant="outline" aria-label="Telefon" rounded="full">
|
||||||
|
<LuPhone />
|
||||||
|
000 000 000
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/Header.js
Normal file
77
src/components/Header.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
Link,
|
||||||
|
Image,
|
||||||
|
Spacer,
|
||||||
|
IconButton,
|
||||||
|
ClientOnly,
|
||||||
|
Skeleton
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { FaCircleUser } from "react-icons/fa6";
|
||||||
|
import { useColorMode } from "@/components/ui/color-mode"
|
||||||
|
import { LuMoon, LuSun } from "react-icons/lu"
|
||||||
|
import styles from "./Header.module.css";
|
||||||
|
import {MENU_ITEMS} from "../lib/constants";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const { toggleColorMode, colorMode } = useColorMode()
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="header"
|
||||||
|
bg="white"
|
||||||
|
boxShadow="sm"
|
||||||
|
position="sticky"
|
||||||
|
top={0}
|
||||||
|
zIndex={10}
|
||||||
|
>
|
||||||
|
{/* First row: Logo + User menu */}
|
||||||
|
<Flex
|
||||||
|
px={8}
|
||||||
|
py={2}
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="gray.100"
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<Link as={NextLink} href="/" _hover={{ textDecoration: "none" }}>
|
||||||
|
<Image src="/logo.svg" alt="Logo" h="40px" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Spacer />
|
||||||
|
|
||||||
|
{/* User Menu */}
|
||||||
|
<IconButton aria-label="Call support" rounded="full">
|
||||||
|
<FaCircleUser />
|
||||||
|
</IconButton>
|
||||||
|
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||||
|
<IconButton onClick={toggleColorMode} variant="outline" size="sm">
|
||||||
|
{colorMode === "light" ? <LuSun /> : <LuMoon />}
|
||||||
|
</IconButton>
|
||||||
|
</ClientOnly>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Second row: Navigation menu */}
|
||||||
|
<Flex px={8} py={3} as="nav">
|
||||||
|
<HStack spacing={6}>
|
||||||
|
{MENU_ITEMS.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
as={NextLink}
|
||||||
|
href={item.href}
|
||||||
|
fontWeight="medium"
|
||||||
|
_hover={{ color: "teal.500" }}
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/components/Header.module.css
Normal file
0
src/components/Header.module.css
Normal file
62
src/components/HeroBanner.js
Normal file
62
src/components/HeroBanner.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {Box, Flex, Button, HStack, Wrap, Link, Text, Heading} from "@chakra-ui/react";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import {RiArrowRightLine, RiMailLine} from "react-icons/ri"
|
||||||
|
import {MENU_ITEMS} from "../lib/constants";
|
||||||
|
|
||||||
|
export default function Banner() {
|
||||||
|
return (
|
||||||
|
<Box position="relative" width="100%" height="400px" overflow="hidden">
|
||||||
|
{/* Background Video */}
|
||||||
|
<Box
|
||||||
|
as="video"
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
objectFit="cover"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
zIndex={0}
|
||||||
|
>
|
||||||
|
<source src="/hero-video.mp4" type="video/mp4"/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Overlay */}
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify="center"
|
||||||
|
align="center"
|
||||||
|
position="relative"
|
||||||
|
zIndex={1}
|
||||||
|
bg="rgba(0, 0, 0, 0.4)"
|
||||||
|
color="white"
|
||||||
|
height="100%"
|
||||||
|
textAlign="center"
|
||||||
|
px={4}
|
||||||
|
>
|
||||||
|
{/* Navigation menu on top of banner */}
|
||||||
|
<Heading as="h1">Vse za vase male in malo vecje zivali</Heading>
|
||||||
|
<Wrap gap={10} justify="center">
|
||||||
|
{MENU_ITEMS.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.href}
|
||||||
|
colorPalette="white"
|
||||||
|
variant="solid"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a href={item.href}><RiMailLine/> {item.label}</a>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
|
|
||||||
|
{/* Example headline in the center */}
|
||||||
|
{/*<Text fontSize="3xl" fontWeight="bold">
|
||||||
|
Welcome to Our Website
|
||||||
|
</Text>*/}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/components/Map.js
Normal file
0
src/components/Map.js
Normal file
147
src/components/Map/CompaniesExplorer.tsx
Normal file
147
src/components/Map/CompaniesExplorer.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Box, Flex, Heading, HStack, Input, Stack, Text, Badge } from "@chakra-ui/react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import type { Company } from "./CompanyMap";
|
||||||
|
|
||||||
|
// dynamic import to avoid SSR issues
|
||||||
|
const CompanyMap = dynamic(() => import("./CompanyMap"), { ssr: false });
|
||||||
|
|
||||||
|
export type CompaniesExplorerProps = {
|
||||||
|
companies: Company[];
|
||||||
|
industries?: Array<Company["industry"]>;
|
||||||
|
initialIndustry?: Company["industry"] | "All";
|
||||||
|
showAutoFit?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIndustries: Array<Company["industry"]> = [
|
||||||
|
"Tech",
|
||||||
|
"Retail",
|
||||||
|
"Manufacturing",
|
||||||
|
"Food",
|
||||||
|
"Healthcare",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function CompaniesExplorer({
|
||||||
|
companies,
|
||||||
|
industries = defaultIndustries,
|
||||||
|
initialIndustry = "All",
|
||||||
|
showAutoFit = true,
|
||||||
|
}: CompaniesExplorerProps) {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [industry, setIndustry] = useState<string>(initialIndustry);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
return companies.filter((c) => {
|
||||||
|
const matchesQ =
|
||||||
|
!q ||
|
||||||
|
c.name.toLowerCase().includes(q) ||
|
||||||
|
(c.address?.toLowerCase().includes(q) ?? false);
|
||||||
|
const matchesInd = industry === "All" || c.industry === industry;
|
||||||
|
return matchesQ && matchesInd;
|
||||||
|
});
|
||||||
|
}, [companies, query, industry]);
|
||||||
|
|
||||||
|
const selected = useMemo(
|
||||||
|
() => filtered.find((c) => c.id === selectedId),
|
||||||
|
[filtered, selectedId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h="100dvh" w="100%" p={4} gap={4} wrap={{ base: "wrap", lg: "nowrap" }}>
|
||||||
|
{/* Left panel */}
|
||||||
|
<Box
|
||||||
|
w={{ base: "100%", lg: "35%" }}
|
||||||
|
h={{ base: "48dvh", lg: "100%" }}
|
||||||
|
borderRadius="2xl"
|
||||||
|
p={4}
|
||||||
|
bg="white"
|
||||||
|
boxShadow="sm"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" mb={3}>
|
||||||
|
<Heading size="md">Companies</Heading>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack mb={3} spacing={2}>
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name or city..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Box as="select"
|
||||||
|
value={industry}
|
||||||
|
onChange={(e) => setIndustry(e.target.value)}
|
||||||
|
maxW="44"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
>
|
||||||
|
<option value="All">All</option>
|
||||||
|
{industries.map((i) => (
|
||||||
|
<option key={i} value={i}>{i}</option>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Box overflowY="auto" h={{ base: "calc(48dvh - 120px)", lg: "calc(100% - 120px)" }} pr={2}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{filtered.map((c) => {
|
||||||
|
const active = c.id === selectedId;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={c.id}
|
||||||
|
p={3}
|
||||||
|
borderRadius="xl"
|
||||||
|
borderWidth={active ? "2px" : "1px"}
|
||||||
|
borderColor={active ? "blue.400" : "gray.200"}
|
||||||
|
bg={active ? "blue.50" : "transparent"}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => setSelectedId(c.id)}
|
||||||
|
_hover={{ borderColor: "blue.300" }}
|
||||||
|
>
|
||||||
|
<HStack justify="space-between" align="start">
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Text fontWeight="semibold">{c.name}</Text>
|
||||||
|
{c.address && (
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
{c.address}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<Badge>{c.industry}</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<Text color="gray.500" textAlign="center" mt={6}>
|
||||||
|
No companies match your filters.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right panel */}
|
||||||
|
<Box
|
||||||
|
flex="1"
|
||||||
|
h={{ base: "48dvh", lg: "100%" }}
|
||||||
|
borderRadius="2xl"
|
||||||
|
overflow="hidden"
|
||||||
|
boxShadow="sm"
|
||||||
|
>
|
||||||
|
<CompanyMap
|
||||||
|
companies={filtered}
|
||||||
|
selectedId={selected?.id ?? null}
|
||||||
|
onSelect={(id) => setSelectedId(id)}
|
||||||
|
autoFit={showAutoFit}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
src/components/Map/CompanyMap.tsx
Normal file
151
src/components/Map/CompanyMap.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Box, Badge, Flex, Text } from "@chakra-ui/react";
|
||||||
|
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
|
||||||
|
import L, { DivIcon, LatLngBoundsLiteral } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.css";
|
||||||
|
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
|
||||||
|
import MarkerClusterGroup from "react-leaflet-markercluster";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
export type Company = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
industry: "Tech" | "Retail" | "Manufacturing" | "Food" | "Healthcare";
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
companies: Company[];
|
||||||
|
selectedId?: string | null;
|
||||||
|
onSelect?: (id: string) => void;
|
||||||
|
autoFit?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const industryEmoji: Record<Company["industry"], string> = {
|
||||||
|
Tech: "💻",
|
||||||
|
Retail: "🛒",
|
||||||
|
Manufacturing: "🏭",
|
||||||
|
Food: "🍽️",
|
||||||
|
Healthcare: "🏥",
|
||||||
|
};
|
||||||
|
|
||||||
|
const industryColor: Record<Company["industry"], string> = {
|
||||||
|
Tech: "#3b82f6",
|
||||||
|
Retail: "#f59e0b",
|
||||||
|
Manufacturing: "#6b7280",
|
||||||
|
Food: "#10b981",
|
||||||
|
Healthcare: "#ef4444",
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeIcon(industry: Company["industry"], highlighted: boolean): DivIcon {
|
||||||
|
const size = highlighted ? 40 : 32;
|
||||||
|
const color = industryColor[industry];
|
||||||
|
const emoji = industryEmoji[industry];
|
||||||
|
const border = highlighted ? `3px solid ${color}` : `2px solid ${color}`;
|
||||||
|
return L.divIcon({
|
||||||
|
className: "company-div-icon",
|
||||||
|
html: `
|
||||||
|
<div style="
|
||||||
|
width:${size}px;height:${size}px;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
background:#fff;border:${border};
|
||||||
|
box-shadow:0 2px 8px rgba(0,0,0,.15);
|
||||||
|
font-size:${Math.round(size * 0.55)}px;
|
||||||
|
">${emoji}</div>
|
||||||
|
`,
|
||||||
|
iconSize: [size, size],
|
||||||
|
iconAnchor: [size / 2, size / 2],
|
||||||
|
popupAnchor: [0, -size / 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlyTo({ lat, lng }: { lat: number; lng: number }) {
|
||||||
|
const map = useMap();
|
||||||
|
useEffect(() => {
|
||||||
|
map.flyTo([lat, lng], Math.max(map.getZoom(), 13), { duration: 0.7 });
|
||||||
|
}, [lat, lng, map]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FitBoundsOnData({ points }: { points: { lat: number; lng: number }[] }) {
|
||||||
|
const map = useMap();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!points?.length) return;
|
||||||
|
let bounds: LatLngBoundsLiteral = [
|
||||||
|
[points[0].lat, points[0].lng],
|
||||||
|
[points[0].lat, points[0].lng],
|
||||||
|
];
|
||||||
|
for (const p of points) {
|
||||||
|
bounds = L.latLngBounds(bounds).extend([p.lat, p.lng]) as any;
|
||||||
|
}
|
||||||
|
map.fitBounds(bounds, { padding: [30, 30] });
|
||||||
|
}, [points, map]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CompanyMap({ companies, selectedId, onSelect, autoFit = true }: Props) {
|
||||||
|
const selected = useMemo(
|
||||||
|
() => companies.find((c) => c.id === selectedId),
|
||||||
|
[companies, selectedId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const center = useMemo<[number, number]>(() => {
|
||||||
|
if (!companies.length) return [46.0569, 14.5058]; // Ljubljana fallback
|
||||||
|
const lat = companies.reduce((a, c) => a + c.lat, 0) / companies.length;
|
||||||
|
const lng = companies.reduce((a, c) => a + c.lng, 0) / companies.length;
|
||||||
|
return [lat, lng];
|
||||||
|
}, [companies]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box w="100%" h="100%" borderRadius="2xl" overflow="hidden" bg="white">
|
||||||
|
<MapContainer
|
||||||
|
center={center}
|
||||||
|
zoom={6}
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
preferCanvas
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution="© OpenStreetMap"
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MarkerClusterGroup chunkedLoading>
|
||||||
|
{companies.map((c) => (
|
||||||
|
<Marker
|
||||||
|
key={c.id}
|
||||||
|
position={[c.lat, c.lng]}
|
||||||
|
icon={makeIcon(c.industry, c.id === selectedId)}
|
||||||
|
eventHandlers={{
|
||||||
|
click: () => onSelect?.(c.id),
|
||||||
|
mouseover: (e) => e.target.openPopup(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popup>
|
||||||
|
<Flex direction="column" gap={1}>
|
||||||
|
<Text fontWeight="bold">{c.name}</Text>
|
||||||
|
<Badge
|
||||||
|
w="fit-content"
|
||||||
|
colorScheme="gray"
|
||||||
|
style={{ background: industryColor[c.industry], color: "#fff" }}
|
||||||
|
>
|
||||||
|
{c.industry}
|
||||||
|
</Badge>
|
||||||
|
{c.address && <Text fontSize="sm">{c.address}</Text>}
|
||||||
|
</Flex>
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</MarkerClusterGroup>
|
||||||
|
|
||||||
|
{autoFit && companies.length > 0 && !selected && (
|
||||||
|
<FitBoundsOnData points={companies.map(({ lat, lng }) => ({ lat, lng }))} />
|
||||||
|
)}
|
||||||
|
{selected && <FlyTo lat={selected.lat} lng={selected.lng} />}
|
||||||
|
</MapContainer>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/Sponsor.js
Normal file
59
src/components/Sponsor.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Container, Stack, Badge, Box, Button, Card, HStack, Image, Heading } from "@chakra-ui/react"
|
||||||
|
|
||||||
|
export default function Sponsor() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Heading size="4xl" as="h2" >Sponzorji meseca</Heading>
|
||||||
|
<Stack direction="row" wrap="wrap" gap={4}>
|
||||||
|
<Card.Root flexDirection="row" overflow="hidden" maxW="xl">
|
||||||
|
<Image
|
||||||
|
objectFit="contain"
|
||||||
|
maxW="200px"
|
||||||
|
src="https://kanimedico.com/wp-content/uploads/2024/12/logo.svg"
|
||||||
|
alt="Kani Medico"
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title mb="2">The perfect latte</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
Caffè latte is a coffee beverage of Italian origin made with espresso
|
||||||
|
and steamed milk.
|
||||||
|
</Card.Description>
|
||||||
|
<HStack mt="4">
|
||||||
|
<Badge>Hot</Badge>
|
||||||
|
<Badge>Caffeine</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer>
|
||||||
|
<Button>Buy Latte</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Box>
|
||||||
|
</Card.Root>
|
||||||
|
<Card.Root flexDirection="row" overflow="hidden" maxW="xl">
|
||||||
|
<Image
|
||||||
|
objectFit="contain"
|
||||||
|
maxW="200px"
|
||||||
|
src="https://www.hepifit.si/wp-content/uploads/2020/02/HepiFit_logo-500x180-1-e1589702333940.png"
|
||||||
|
alt="Hepi Fit"
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title mb="2">The perfect latte</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
Caffè latte is a coffee beverage of Italian origin made with espresso
|
||||||
|
and steamed milk.
|
||||||
|
</Card.Description>
|
||||||
|
<HStack mt="4">
|
||||||
|
<Badge>Hot</Badge>
|
||||||
|
<Badge>Caffeine</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Card.Body>
|
||||||
|
<Card.Footer>
|
||||||
|
<Button>Buy Latte</Button>
|
||||||
|
</Card.Footer>
|
||||||
|
</Box>
|
||||||
|
</Card.Root>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/lib/constants.js
Normal file
42
src/lib/constants.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const MENU_ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Veterinarji",
|
||||||
|
icon: "",
|
||||||
|
href: "/veterinarji"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Trgovine",
|
||||||
|
icon: "",
|
||||||
|
href: "/trgovine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Vzgoja",
|
||||||
|
icon: "",
|
||||||
|
href: "/vzgoja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Varstvo in sprehajanje",
|
||||||
|
icon: "",
|
||||||
|
href: "/varstvo-in-sprehajanje"
|
||||||
|
},{
|
||||||
|
label: "Frizerji",
|
||||||
|
icon: "",
|
||||||
|
href: "/frizerji"
|
||||||
|
},{
|
||||||
|
label: "Društva",
|
||||||
|
icon: "",
|
||||||
|
href: "/drustva"
|
||||||
|
},{
|
||||||
|
label: "Rejci živali",
|
||||||
|
icon: "",
|
||||||
|
href: "/rejci-zivali"
|
||||||
|
},{
|
||||||
|
label: "Zavetišča",
|
||||||
|
icon: "",
|
||||||
|
href: "/zavetisca"
|
||||||
|
},{
|
||||||
|
label: "Ostalo",
|
||||||
|
icon: "",
|
||||||
|
href: "/ostalo"
|
||||||
|
},
|
||||||
|
]
|
||||||
29
src/utils/supabase/server.ts
Normal file
29
src/utils/supabase/server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createServerClient } from '@supabase/ssr'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
export async function createClient() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
|
||||||
|
return createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return cookieStore.getAll()
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet) {
|
||||||
|
try {
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
cookieStore.set(name, value, options)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// The `setAll` method was called from a Server Component.
|
||||||
|
// This can be ignored if you have middleware refreshing
|
||||||
|
// user sessions.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user