adding lcoation
This commit is contained in:
parent
ee9edc466e
commit
f047fd0304
1
.env
1
.env
@ -5,3 +5,4 @@ MY_AWS_SECRET_KEY="SwS8fm1+pKlrWU8pzuRCUYqyJ5roNwu9AZRhbWMu"
|
||||
REGION="us-east-1"
|
||||
S3_BUCKET_URL="https://flockstr.s3.amazonaws.com"
|
||||
NEXT_PUBLIC_S3_BUCKET_URL="https://flockstr.s3.amazonaws.com"
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_KEY=AIzaSyDehwC0_6GIfPRZgNm4cpWuQ_Dz1HcEoYg
|
@ -123,3 +123,7 @@
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
}
|
||||
input[type="time"]::-webkit-calendar-picker-indicator {
|
||||
background: none;
|
||||
display: none;
|
||||
}
|
||||
|
19
components/EventIcons/DateIcon.tsx
Normal file
19
components/EventIcons/DateIcon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { formatDate } from "@/lib/utils/dates";
|
||||
|
||||
type SmallCalendarIconProps = {
|
||||
date: Date;
|
||||
};
|
||||
export default function SmallCalendarIcon({ date }: SmallCalendarIconProps) {
|
||||
return (
|
||||
<div className="center h-10 w-10 overflow-hidden rounded-sm border text-muted-foreground">
|
||||
<div className="w-full text-center">
|
||||
<div className="bg-muted p-[2px] text-[10px] font-semibold uppercase">
|
||||
{formatDate(date, "MMM")}
|
||||
</div>
|
||||
<div className="text-center text-[14px] font-medium">
|
||||
{formatDate(date, "D")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
13
components/EventIcons/LocationIcon.tsx
Normal file
13
components/EventIcons/LocationIcon.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { HiOutlineMapPin } from "react-icons/hi2";
|
||||
|
||||
export default function LocationIcon() {
|
||||
return (
|
||||
<div className="center h-10 w-10 overflow-hidden rounded-sm border bg-muted text-muted-foreground">
|
||||
<div className="center w-full text-center">
|
||||
<div className="text-center text-[14px] font-medium">
|
||||
<HiOutlineMapPin className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
121
components/LocationSearch/index.tsx
Normal file
121
components/LocationSearch/index.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
import { useMemo } from "react";
|
||||
import usePlacesAutocomplete from "use-places-autocomplete";
|
||||
import { Input } from "../ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HiOutlineBuildingStorefront } from "react-icons/hi2";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { useLoadScript } from "@react-google-maps/api";
|
||||
|
||||
export default function LocationSearchInput() {
|
||||
const libraries = useMemo(() => ["places"], []);
|
||||
const { isLoaded, loadError } = useLoadScript({
|
||||
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY as string,
|
||||
libraries: libraries as any,
|
||||
});
|
||||
const {
|
||||
ready,
|
||||
value,
|
||||
suggestions: { status, data }, // results from Google Places API for the given search term
|
||||
setValue, // use this method to link input value with the autocomplete hook
|
||||
clearSuggestions,
|
||||
} = usePlacesAutocomplete({
|
||||
requestOptions: { componentRestrictions: { country: "us" } }, // restrict search to US
|
||||
debounce: 300,
|
||||
cache: 86400,
|
||||
});
|
||||
if (loadError) {
|
||||
return <p>hello{JSON.stringify(loadError)}</p>;
|
||||
}
|
||||
if (!isLoaded) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
return <CommandSearch />;
|
||||
}
|
||||
function CommandSearch() {
|
||||
const {
|
||||
ready,
|
||||
value,
|
||||
suggestions: { status, data }, // results from Google Places API for the given search term
|
||||
setValue, // use this method to link input value with the autocomplete hook
|
||||
clearSuggestions,
|
||||
} = usePlacesAutocomplete({
|
||||
requestOptions: { componentRestrictions: { country: "us" } }, // restrict search to US
|
||||
debounce: 300,
|
||||
cache: 86400,
|
||||
});
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"justify-start p-0 text-left font-normal",
|
||||
!value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Add a location...
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-modal+ w-auto p-0" align="start">
|
||||
<Command className="rounded-lg border shadow-md">
|
||||
<CommandInput
|
||||
disabled={!ready}
|
||||
onChangeCapture={(e) =>
|
||||
setValue((e.target as unknown as { value: string }).value)
|
||||
}
|
||||
value={value}
|
||||
placeholder="Search places..."
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
{data.map(
|
||||
({
|
||||
description,
|
||||
place_id,
|
||||
structured_formatting: { main_text, secondary_text },
|
||||
}) => (
|
||||
<CommandItem key={place_id}>
|
||||
<HiOutlineBuildingStorefront className="mr-2 h-4 w-4" />
|
||||
<span>{main_text}</span>
|
||||
<span>{description}</span>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
{/* <CommandSeparator />
|
||||
<CommandGroup heading="Vitrual">
|
||||
<CommandItem>
|
||||
<HiOutlineBuildingStorefront className="mr-2 h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<HiOutlineBuildingStorefront className="mr-2 h-4 w-4" />
|
||||
<span>Mail</span>
|
||||
</CommandItem>
|
||||
<CommandItem>
|
||||
<HiOutlineBuildingStorefront className="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</CommandItem>
|
||||
</CommandGroup> */}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
207
components/Modals/CreateCalendarEvent.tsx
Normal file
207
components/Modals/CreateCalendarEvent.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Template from "./Template";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HiX } from "react-icons/hi";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
addMinutesToDate,
|
||||
convertToTimezoneDate,
|
||||
convertToTimezone,
|
||||
} from "@/lib/utils/dates";
|
||||
import { DatePicker } from "@/components/ui/date-picker";
|
||||
import { TimePicker } from "@/components/ui/time-picker";
|
||||
import { TimezoneSelector } from "../ui/timezone";
|
||||
import SmallCalendarIcon from "../EventIcons/DateIcon";
|
||||
import LocationIcon from "../EventIcons/LocationIcon";
|
||||
import LocationSearchInput from "../LocationSearch";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
|
||||
export default function CreateCalendarEventModal() {
|
||||
const modal = useModal();
|
||||
const now = new Date(new Date().setHours(12, 0, 0, 0));
|
||||
const [title, setTitle] = useState("");
|
||||
const [startDate, setStartDate] = useState<Date>(now);
|
||||
const startTime = `${
|
||||
startDate?.getHours().toLocaleString().length === 1
|
||||
? "0" + startDate?.getHours().toLocaleString()
|
||||
: startDate?.getHours()
|
||||
}:${
|
||||
startDate?.getMinutes().toLocaleString().length === 1
|
||||
? "0" + startDate?.getMinutes().toLocaleString()
|
||||
: startDate?.getMinutes()
|
||||
}`;
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(
|
||||
new Date(new Date().setHours(13)),
|
||||
);
|
||||
const endTime = `${
|
||||
endDate?.getHours().toLocaleString().length === 1
|
||||
? "0" + endDate?.getHours().toLocaleString()
|
||||
: endDate?.getHours()
|
||||
}:${
|
||||
endDate?.getMinutes().toLocaleString().length === 1
|
||||
? "0" + endDate?.getMinutes().toLocaleString()
|
||||
: endDate?.getMinutes()
|
||||
}`;
|
||||
const [timezone, setTimezone] = useState(
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) {
|
||||
if (startDate.getTime() > endDate.getTime()) {
|
||||
setEndDate(addMinutesToDate(startDate, 60));
|
||||
}
|
||||
}
|
||||
}, [startDate]);
|
||||
|
||||
const titleRef = useRef<HTMLTextAreaElement>(null);
|
||||
useAutosizeTextArea(titleRef.current, title);
|
||||
|
||||
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full grow bg-background p-4 shadow md:rounded-lg md:border md:p-6",
|
||||
"md:max-w-[600px]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => modal?.hide()}
|
||||
className="absolute right-4 top-4 hidden text-muted-foreground transition-all hover:text-primary md:flex"
|
||||
>
|
||||
<HiX className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="">
|
||||
<Textarea
|
||||
ref={titleRef}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Event Name"
|
||||
className={cn(
|
||||
"resize-none break-words border-0 bg-transparent p-0 text-3xl font-bold text-foreground shadow-none outline-none placeholder:text-muted-foreground/50 placeholder:hover:text-muted-foreground/80 focus-visible:ring-0",
|
||||
title === "" && "max-h-[60px]",
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="flex w-full items-start gap-x-3">
|
||||
<div className="shrink-0">
|
||||
<SmallCalendarIcon date={startDate ?? new Date()} />
|
||||
</div>
|
||||
<div className="max-w-[300px] flex-1 divide-y overflow-hidden rounded-md bg-muted">
|
||||
<div className="flex justify-between p-0.5 px-1 pl-3">
|
||||
<div className="flex w-[70px] shrink-0 items-center">Start</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex max-w-full bg-secondary">
|
||||
<DatePicker
|
||||
displayFormat="ddd, MMM D"
|
||||
date={startDate}
|
||||
onDateChange={(newDate) =>
|
||||
setStartDate((prev) => {
|
||||
if (!prev || !newDate) return newDate ?? now;
|
||||
return new Date(
|
||||
newDate.setHours(
|
||||
prev.getHours(),
|
||||
prev.getMinutes(),
|
||||
),
|
||||
);
|
||||
})
|
||||
}
|
||||
hideIcon={true}
|
||||
/>
|
||||
<TimePicker
|
||||
className="max-w-fit pl-0 pr-1"
|
||||
value={startTime}
|
||||
onChange={(newTime) =>
|
||||
setStartDate(
|
||||
(prev) =>
|
||||
new Date(
|
||||
prev!.setHours(
|
||||
parseInt(newTime.split(":")[0] as string),
|
||||
parseInt(newTime.split(":")[1] as string),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
hideIcon={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between p-0.5 px-1 pl-3">
|
||||
<div className="flex w-[70px] shrink-0 items-center">End</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex max-w-full bg-secondary">
|
||||
<DatePicker
|
||||
displayFormat="ddd, MMM D"
|
||||
date={endDate}
|
||||
onDateChange={(newDate) =>
|
||||
setEndDate((prev) => {
|
||||
if (!prev || !newDate) return newDate ?? now;
|
||||
return new Date(
|
||||
newDate.setHours(
|
||||
prev.getHours(),
|
||||
prev.getMinutes(),
|
||||
),
|
||||
);
|
||||
})
|
||||
}
|
||||
hideIcon={true}
|
||||
/>
|
||||
<TimePicker
|
||||
className="max-w-fit pl-0 pr-1"
|
||||
value={endTime}
|
||||
onChange={(newTime) =>
|
||||
setEndDate(
|
||||
(prev) =>
|
||||
new Date(
|
||||
prev!.setHours(
|
||||
parseInt(newTime.split(":")[0] as string),
|
||||
parseInt(newTime.split(":")[1] as string),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
hideIcon={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between p-0.5 px-1 pl-3">
|
||||
<div className="flex-1 text-xs text-muted-foreground">
|
||||
<div className="flex max-w-full justify-start bg-secondary">
|
||||
<TimezoneSelector
|
||||
hideIcon={false}
|
||||
className="px-0 pr-1 font-normal"
|
||||
value={timezone}
|
||||
onChange={(newTimezone) => setTimezone(newTimezone)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-start gap-x-3">
|
||||
<div className="shrink-0">
|
||||
<LocationIcon />
|
||||
</div>
|
||||
<div className="max-w-[300px] flex-1 divide-y overflow-hidden rounded-md bg-muted">
|
||||
<div className="flex justify-between p-0.5 px-1 pl-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex max-w-full bg-secondary">
|
||||
<LocationSearchInput />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -19,6 +19,7 @@ import { formatCount } from "@/lib/utils";
|
||||
import LoginModal from "./Login";
|
||||
import CreateList from "./CreateList";
|
||||
import ShortTextNoteModal from "./ShortTextNote";
|
||||
import CreateCalendarEventModal from "./CreateCalendarEvent";
|
||||
export default function NewEventModal() {
|
||||
const modal = useModal();
|
||||
return (
|
||||
@ -33,8 +34,17 @@ export default function NewEventModal() {
|
||||
<span>Short Text</span>
|
||||
<HiChatBubbleLeftEllipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal?.swap(<CreateCalendarEventModal />);
|
||||
}}
|
||||
className="w-full gap-x-1"
|
||||
>
|
||||
<span>Calendar Event</span>
|
||||
<HiChatBubbleLeftEllipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
<Link href={`/article/new`}>
|
||||
<Button className="w-full gap-x-1">
|
||||
<Button onClick={() => modal?.hide()} className="w-full gap-x-1">
|
||||
<span>Long Form</span>
|
||||
<HiNewspaper className="h-4 w-4" />
|
||||
</Button>
|
||||
|
58
components/ui/date-picker.tsx
Normal file
58
components/ui/date-picker.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { CalendarIcon } from "@radix-ui/react-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate } from "@/lib/utils/dates";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
type DatePickerProps = {
|
||||
date: Date | undefined;
|
||||
onDateChange: (newDate: Date | undefined) => void;
|
||||
initialFocus?: boolean;
|
||||
placeholder?: string;
|
||||
hideIcon?: boolean;
|
||||
displayFormat?: string;
|
||||
};
|
||||
|
||||
export function DatePicker({
|
||||
date,
|
||||
onDateChange,
|
||||
placeholder = "Pick a date",
|
||||
initialFocus,
|
||||
hideIcon = false,
|
||||
displayFormat,
|
||||
}: DatePickerProps) {
|
||||
// const [date, setDate] = React.useState<Date>();
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"w-full justify-end pr-1 text-left font-normal",
|
||||
!date && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <CalendarIcon className="mr-2 h-4 w-4" />}
|
||||
{date ? formatDate(date, displayFormat) : <span>{placeholder}</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-modal+ w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={onDateChange}
|
||||
initialFocus={initialFocus}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
133
components/ui/time-picker.tsx
Normal file
133
components/ui/time-picker.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { CalendarIcon } from "@radix-ui/react-icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate } from "@/lib/utils/dates";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
|
||||
const times = [
|
||||
{ label: "12:00 AM", value: "00:00" },
|
||||
{ label: "12:30 AM", value: "00:30" },
|
||||
{ label: "1:00 AM", value: "01:00" },
|
||||
{ label: "1:30 AM", value: "01:30" },
|
||||
{ label: "2:00 AM", value: "02:00" },
|
||||
{ label: "2:30 AM", value: "02:30" },
|
||||
{ label: "3:00 AM", value: "03:00" },
|
||||
{ label: "3:30 AM", value: "03:30" },
|
||||
{ label: "4:00 AM", value: "04:00" },
|
||||
{ label: "4:30 AM", value: "04:30" },
|
||||
{ label: "5:00 AM", value: "05:00" },
|
||||
{ label: "5:30 AM", value: "05:30" },
|
||||
{ label: "6:00 AM", value: "06:00" },
|
||||
{ label: "6:30 AM", value: "06:30" },
|
||||
{ label: "7:00 AM", value: "07:00" },
|
||||
{ label: "7:30 AM", value: "07:30" },
|
||||
{ label: "8:00 AM", value: "08:00" },
|
||||
{ label: "8:30 AM", value: "08:30" },
|
||||
{ label: "9:00 AM", value: "09:00" },
|
||||
{ label: "9:30 AM", value: "09:30" },
|
||||
{ label: "10:00 AM", value: "10:00" },
|
||||
{ label: "10:30 AM", value: "10:30" },
|
||||
{ label: "11:00 AM", value: "11:00" },
|
||||
{ label: "11:30 AM", value: "11:30" },
|
||||
{ label: "12:00 PM", value: "12:00" },
|
||||
{ label: "12:30 PM", value: "12:30" },
|
||||
{ label: "1:00 PM", value: "13:00" },
|
||||
{ label: "1:30 PM", value: "13:30" },
|
||||
{ label: "2:00 PM", value: "14:00" },
|
||||
{ label: "2:30 PM", value: "14:30" },
|
||||
{ label: "3:00 PM", value: "15:00" },
|
||||
{ label: "3:30 PM", value: "15:30" },
|
||||
{ label: "4:00 PM", value: "16:00" },
|
||||
{ label: "4:30 PM", value: "16:30" },
|
||||
{ label: "5:00 PM", value: "17:00" },
|
||||
{ label: "5:30 PM", value: "17:30" },
|
||||
{ label: "6:00 PM", value: "18:00" },
|
||||
{ label: "6:30 PM", value: "18:30" },
|
||||
{ label: "7:00 PM", value: "19:00" },
|
||||
{ label: "7:30 PM", value: "19:30" },
|
||||
{ label: "8:00 PM", value: "20:00" },
|
||||
{ label: "8:30 PM", value: "20:30" },
|
||||
{ label: "9:00 PM", value: "21:00" },
|
||||
{ label: "9:30 PM", value: "21:30" },
|
||||
{ label: "10:00 PM", value: "22:00" },
|
||||
{ label: "10:30 PM", value: "22:30" },
|
||||
{ label: "11:00 PM", value: "23:00" },
|
||||
{ label: "11:30 PM", value: "23:30" },
|
||||
] as const;
|
||||
|
||||
type TimePickerProps = {
|
||||
value: string | undefined;
|
||||
onChange: (newTime: string) => void;
|
||||
initialFocus?: boolean;
|
||||
placeholder?: string;
|
||||
hideIcon?: boolean;
|
||||
displayFormat?: string;
|
||||
className?: string;
|
||||
};
|
||||
export function TimePicker({
|
||||
value,
|
||||
onChange,
|
||||
hideIcon = false,
|
||||
className,
|
||||
}: TimePickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className={cn(
|
||||
"justify-start p-0 text-left font-normal",
|
||||
!value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <CalendarIcon className="mr-2 h-4 w-4" />}
|
||||
<Input
|
||||
type="time"
|
||||
placeholder="HH:MM"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
console.log(e.target.valueAsNumber);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
className={cn(
|
||||
"border-0 shadow-none outline-none ring-0 focus-visible:ring-0",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-modal+ w-auto p-0" align="start">
|
||||
<Command className="max-h-[200px]">
|
||||
<CommandGroup className="overflow-y-auto">
|
||||
{times.map((time) => (
|
||||
<CommandItem
|
||||
value={time.label}
|
||||
key={time.value}
|
||||
onSelect={() => {
|
||||
onChange(time.value);
|
||||
}}
|
||||
>
|
||||
{time.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
90
components/ui/timezone.tsx
Normal file
90
components/ui/timezone.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HiOutlineGlobeAlt, HiChevronDown, HiCheck } from "react-icons/hi2";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { TIMEZONES } from "@/constants/timezones";
|
||||
const timezones = TIMEZONES.filter((t) =>
|
||||
Intl.supportedValuesOf("timeZone").includes(t.name),
|
||||
).map((t) => ({
|
||||
label: `(${t.offset}) ${t.name.split("/").pop()?.replaceAll("_", " ")}`,
|
||||
value: t.name,
|
||||
}));
|
||||
|
||||
type TimezoneSelectorProps = {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
className?: string;
|
||||
hideIcon?: boolean;
|
||||
};
|
||||
export function TimezoneSelector({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
hideIcon = false,
|
||||
}: TimezoneSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
>
|
||||
{!hideIcon && <HiOutlineGlobeAlt className="h-4 w-4 shrink-0" />}
|
||||
<span className="shrink truncate">
|
||||
{value
|
||||
? timezones.find((timezone) => timezone.value === value)?.label
|
||||
: "Select timezone..."}
|
||||
</span>
|
||||
{!hideIcon && (
|
||||
<HiChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-modal+ w-[250px] p-0">
|
||||
<Command className="max-h-[200px]">
|
||||
<CommandInput placeholder="Search timezone..." className="h-9" />
|
||||
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||
<CommandGroup className="overflow-y-auto">
|
||||
{timezones.map((timezone) => (
|
||||
<CommandItem
|
||||
key={timezone.value}
|
||||
value={timezone.value}
|
||||
onSelect={() => {
|
||||
onChange(timezone.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{timezone.label}
|
||||
<HiCheck
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 text-primary",
|
||||
value === timezone.value ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
374
constants/timezones.ts
Normal file
374
constants/timezones.ts
Normal file
@ -0,0 +1,374 @@
|
||||
export const TIMEZONES = [
|
||||
{
|
||||
offset: "GMT-12:00",
|
||||
name: "Etc/GMT-12",
|
||||
},
|
||||
{
|
||||
offset: "GMT-11:00",
|
||||
name: "Etc/GMT-11",
|
||||
},
|
||||
{
|
||||
offset: "GMT-11:00",
|
||||
name: "Pacific/Midway",
|
||||
},
|
||||
{
|
||||
offset: "GMT-10:00",
|
||||
name: "America/Adak",
|
||||
},
|
||||
{
|
||||
offset: "GMT-09:00",
|
||||
name: "America/Anchorage",
|
||||
},
|
||||
{
|
||||
offset: "GMT-09:00",
|
||||
name: "Pacific/Gambier",
|
||||
},
|
||||
{
|
||||
offset: "GMT-08:00",
|
||||
name: "America/Dawson_Creek",
|
||||
},
|
||||
{
|
||||
offset: "GMT-08:00",
|
||||
name: "America/Ensenada",
|
||||
},
|
||||
{
|
||||
offset: "GMT-08:00",
|
||||
name: "America/Los_Angeles",
|
||||
},
|
||||
{
|
||||
offset: "GMT-07:00",
|
||||
name: "America/Chihuahua",
|
||||
},
|
||||
{
|
||||
offset: "GMT-07:00",
|
||||
name: "America/Denver",
|
||||
},
|
||||
{
|
||||
offset: "GMT-06:00",
|
||||
name: "America/Belize",
|
||||
},
|
||||
{
|
||||
offset: "GMT-06:00",
|
||||
name: "America/Cancun",
|
||||
},
|
||||
{
|
||||
offset: "GMT-06:00",
|
||||
name: "America/Chicago",
|
||||
},
|
||||
{
|
||||
offset: "GMT-06:00",
|
||||
name: "Chile/EasterIsland",
|
||||
},
|
||||
{
|
||||
offset: "GMT-05:00",
|
||||
name: "America/Bogota",
|
||||
},
|
||||
{
|
||||
offset: "GMT-05:00",
|
||||
name: "America/Havana",
|
||||
},
|
||||
{
|
||||
offset: "GMT-05:00",
|
||||
name: "America/New_York",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:30",
|
||||
name: "America/Caracas",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:00",
|
||||
name: "America/Campo_Grande",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:00",
|
||||
name: "America/Glace_Bay",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:00",
|
||||
name: "America/Goose_Bay",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:00",
|
||||
name: "America/Santiago",
|
||||
},
|
||||
{
|
||||
offset: "GMT-04:00",
|
||||
name: "America/La_Paz",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Argentina/Buenos_Aires",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Montevideo",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Araguaina",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Godthab",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Miquelon",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:00",
|
||||
name: "America/Sao_Paulo",
|
||||
},
|
||||
{
|
||||
offset: "GMT-03:30",
|
||||
name: "America/St_Johns",
|
||||
},
|
||||
{
|
||||
offset: "GMT-02:00",
|
||||
name: "America/Noronha",
|
||||
},
|
||||
{
|
||||
offset: "GMT-01:00",
|
||||
name: "Atlantic/Cape_Verde",
|
||||
},
|
||||
{
|
||||
offset: "GMT",
|
||||
name: "Europe/Belfast",
|
||||
},
|
||||
{
|
||||
offset: "GMT",
|
||||
name: "Africa/Abidjan",
|
||||
},
|
||||
{
|
||||
offset: "GMT",
|
||||
name: "Europe/Dublin",
|
||||
},
|
||||
{
|
||||
offset: "GMT",
|
||||
name: "Europe/Lisbon",
|
||||
},
|
||||
{
|
||||
offset: "GMT",
|
||||
name: "Europe/London",
|
||||
},
|
||||
{
|
||||
offset: "UTC",
|
||||
name: "UTC",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Africa/Algiers",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Africa/Windhoek",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Atlantic/Azores",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Atlantic/Stanley",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Europe/Amsterdam",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Europe/Belgrade",
|
||||
},
|
||||
{
|
||||
offset: "GMT+01:00",
|
||||
name: "Europe/Brussels",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Africa/Cairo",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Africa/Blantyre",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Asia/Beirut",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Asia/Damascus",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Asia/Gaza",
|
||||
},
|
||||
{
|
||||
offset: "GMT+02:00",
|
||||
name: "Asia/Jerusalem",
|
||||
},
|
||||
{
|
||||
offset: "GMT+03:00",
|
||||
name: "Africa/Addis_Ababa",
|
||||
},
|
||||
{
|
||||
offset: "GMT+03:00",
|
||||
name: "Asia/Riyadh89",
|
||||
},
|
||||
{
|
||||
offset: "GMT+03:00",
|
||||
name: "Europe/Minsk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+03:30",
|
||||
name: "Asia/Tehran",
|
||||
},
|
||||
{
|
||||
offset: "GMT+04:00",
|
||||
name: "Asia/Dubai",
|
||||
},
|
||||
{
|
||||
offset: "GMT+04:00",
|
||||
name: "Asia/Yerevan",
|
||||
},
|
||||
{
|
||||
offset: "GMT+04:00",
|
||||
name: "Europe/Moscow",
|
||||
},
|
||||
{
|
||||
offset: "GMT+04:30",
|
||||
name: "Asia/Kabul",
|
||||
},
|
||||
{
|
||||
offset: "GMT+05:00",
|
||||
name: "Asia/Tashkent",
|
||||
},
|
||||
{
|
||||
offset: "GMT+05:30",
|
||||
name: "Asia/Kolkata",
|
||||
},
|
||||
{
|
||||
offset: "GMT+05:45",
|
||||
name: "Asia/Katmandu",
|
||||
},
|
||||
{
|
||||
offset: "GMT+06:00",
|
||||
name: "Asia/Dhaka",
|
||||
},
|
||||
{
|
||||
offset: "GMT+06:00",
|
||||
name: "Asia/Yekaterinburg",
|
||||
},
|
||||
{
|
||||
offset: "GMT+06:30",
|
||||
name: "Asia/Rangoon",
|
||||
},
|
||||
{
|
||||
offset: "GMT+07:00",
|
||||
name: "Asia/Bangkok",
|
||||
},
|
||||
{
|
||||
offset: "GMT+07:00",
|
||||
name: "Asia/Novosibirsk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+08:00",
|
||||
name: "Etc/GMT+8",
|
||||
},
|
||||
{
|
||||
offset: "GMT+08:00",
|
||||
name: "Asia/Hong_Kong",
|
||||
},
|
||||
{
|
||||
offset: "GMT+08:00",
|
||||
name: "Asia/Krasnoyarsk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+08:00",
|
||||
name: "Australia/Perth",
|
||||
},
|
||||
{
|
||||
offset: "GMT+08:45",
|
||||
name: "Australia/Eucla",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:00",
|
||||
name: "Asia/Irkutsk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:00",
|
||||
name: "Asia/Seoul",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:00",
|
||||
name: "Asia/Tokyo",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:30",
|
||||
name: "Australia/Adelaide",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:30",
|
||||
name: "Australia/Darwin",
|
||||
},
|
||||
{
|
||||
offset: "GMT+09:30",
|
||||
name: "Pacific/Marquesas",
|
||||
},
|
||||
{
|
||||
offset: "GMT+10:00",
|
||||
name: "Etc/GMT+10",
|
||||
},
|
||||
{
|
||||
offset: "GMT+10:00",
|
||||
name: "Australia/Brisbane",
|
||||
},
|
||||
{
|
||||
offset: "GMT+10:00",
|
||||
name: "Australia/Hobart",
|
||||
},
|
||||
{
|
||||
offset: "GMT+10:00",
|
||||
name: "Asia/Yakutsk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+10:30",
|
||||
name: "Australia/Lord_Howe",
|
||||
},
|
||||
{
|
||||
offset: "GMT+11:00",
|
||||
name: "Asia/Vladivostok",
|
||||
},
|
||||
{
|
||||
offset: "GMT+11:30",
|
||||
name: "Pacific/Norfolk",
|
||||
},
|
||||
{
|
||||
offset: "GMT+12:00",
|
||||
name: "Etc/GMT+12",
|
||||
},
|
||||
{
|
||||
offset: "GMT+12:00",
|
||||
name: "Asia/Anadyr",
|
||||
},
|
||||
{
|
||||
offset: "GMT+12:00",
|
||||
name: "Asia/Magadan",
|
||||
},
|
||||
{
|
||||
offset: "GMT+12:00",
|
||||
name: "Pacific/Auckland",
|
||||
},
|
||||
{
|
||||
offset: "GMT+12:45",
|
||||
name: "Pacific/Chatham",
|
||||
},
|
||||
{
|
||||
offset: "GMT+13:00",
|
||||
name: "Pacific/Tongatapu",
|
||||
},
|
||||
{
|
||||
offset: "GMT+14:00",
|
||||
name: "Pacific/Kiritimati",
|
||||
},
|
||||
];
|
@ -3,6 +3,7 @@ import relative from "dayjs/plugin/relativeTime";
|
||||
import updateLocale from "dayjs/plugin/updateLocale";
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
export function relativeTimeUnix(timestamp: number) {
|
||||
const config = {
|
||||
@ -79,3 +80,63 @@ export function formatDate(timestamp: Date, format?: string) {
|
||||
dayjs.extend(timezone);
|
||||
return dayjs(timestamp).format(format ?? "MMMM Do, YYYY");
|
||||
}
|
||||
export function convertToTimezoneDate(inputDate: Date, _timezone: string) {
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
console.log("time", _timezone);
|
||||
return dayjs(inputDate).tz(_timezone).toDate();
|
||||
}
|
||||
|
||||
export function addMinutesToDate(inputDate: Date, minutesToAdd: number) {
|
||||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error("Invalid date input");
|
||||
}
|
||||
|
||||
if (typeof minutesToAdd !== "number" || isNaN(minutesToAdd)) {
|
||||
throw new Error("Invalid minutes input");
|
||||
}
|
||||
// Copy the input date to avoid modifying the original date
|
||||
const resultDate = new Date(inputDate);
|
||||
|
||||
// Add the specified number of minutes
|
||||
resultDate.setMinutes(resultDate.getMinutes() + minutesToAdd);
|
||||
|
||||
return resultDate;
|
||||
}
|
||||
|
||||
export function toUnix(inputDate: Date) {
|
||||
return dayjs(inputDate).unix();
|
||||
}
|
||||
function timezoneDiff(ianatz: string) {
|
||||
const date = new Date();
|
||||
// suppose the date is 12:00 UTC
|
||||
var invdate = new Date(
|
||||
date.toLocaleString("en-US", {
|
||||
timeZone: ianatz,
|
||||
}),
|
||||
);
|
||||
|
||||
// then invdate will be 07:00 in Toronto
|
||||
// and the diff is 5 hours
|
||||
var diff = date.getTime() - invdate.getTime();
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
export function convertToTimezone(inputDate: Date, targetTimezone: string) {
|
||||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error("Invalid date input");
|
||||
}
|
||||
|
||||
if (typeof targetTimezone !== "string") {
|
||||
throw new Error("Invalid timezone input");
|
||||
}
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
// Get plain date w/o timezones
|
||||
const initialDate = new Date(inputDate + "Z");
|
||||
const plainString = initialDate.toISOString().split(".")[0] as string;
|
||||
const plain = dayjs.tz(plainString, targetTimezone);
|
||||
return plain.toDate();
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-google-maps/api": "^2.19.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"aws-sdk": "^2.1475.0",
|
||||
"buffer": "^6.0.3",
|
||||
@ -50,16 +51,19 @@
|
||||
"nostr-tools": "^1.16.0",
|
||||
"ramda": "^0.29.1",
|
||||
"react": "^18",
|
||||
"react-aria": "^3.29.1",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-player": "^2.13.0",
|
||||
"react-stately": "^3.27.1",
|
||||
"recharts": "^2.9.0",
|
||||
"sonner": "^1.0.3",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-places-autocomplete": "^4.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-fetch": "^0.1.1",
|
||||
"zustand": "^4.4.3"
|
||||
|
Loading…
x
Reference in New Issue
Block a user