initian commit
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user