diff --git a/apps/api/prisma/migrations/20230910082136_cad_time_zone/migration.sql b/apps/api/prisma/migrations/20230910082136_cad_time_zone/migration.sql
new file mode 100644
index 000000000..e14758c16
--- /dev/null
+++ b/apps/api/prisma/migrations/20230910082136_cad_time_zone/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "cad" ADD COLUMN "timeZone" TEXT;
diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma
index da2038587..51a0841d0 100644
--- a/apps/api/prisma/schema.prisma
+++ b/apps/api/prisma/schema.prisma
@@ -40,6 +40,7 @@ model cad {
autoSetUserPropertiesId String?
discordRoles DiscordRoles? @relation(fields: [discordRolesId], references: [id])
discordRolesId String?
+ timeZone String?
}
model CadFeature {
diff --git a/apps/api/src/controllers/admin/manage/cad-settings/CadSettings.ts b/apps/api/src/controllers/admin/manage/cad-settings/CadSettings.ts
index defa6823b..b959a1495 100644
--- a/apps/api/src/controllers/admin/manage/cad-settings/CadSettings.ts
+++ b/apps/api/src/controllers/admin/manage/cad-settings/CadSettings.ts
@@ -149,6 +149,7 @@ export class CADSettingsController {
businessWhitelisted: data.businessWhitelisted,
registrationCode: data.registrationCode,
logoId: data.image,
+ timeZone: data.timeZone || null,
miscCadSettings: { update: { roleplayEnabled: data.roleplayEnabled } },
},
include: { features: true, miscCadSettings: true, apiToken: true },
diff --git a/apps/api/src/middlewares/auth/is-auth.ts b/apps/api/src/middlewares/auth/is-auth.ts
index e9af52048..ce497975b 100644
--- a/apps/api/src/middlewares/auth/is-auth.ts
+++ b/apps/api/src/middlewares/auth/is-auth.ts
@@ -128,6 +128,7 @@ export function CAD_SELECT(options: CadSelectOptions) {
businessWhitelisted: true,
features: true,
autoSetUserProperties: true,
+ timeZone: true,
registrationCode: options.selectCADsettings,
steamApiKey: options.selectCADsettings,
apiTokenId: options.selectCADsettings,
diff --git a/apps/client/locales/en/cad-settings.json b/apps/client/locales/en/cad-settings.json
index 005b60ce5..ac227fce3 100644
--- a/apps/client/locales/en/cad-settings.json
+++ b/apps/client/locales/en/cad-settings.json
@@ -23,7 +23,9 @@
"taxiWhitelist": "Taxi Whitelist",
"taxiWhitelistDescription": "The taxi system will be whitelisted. The Taxi permission must be given to the user before they can use the taxi system.",
"businessWhitelist": "Business Whitelist",
- "businessWhitelistDescription": "The business system will be whitelisted. Any new business must first be reviewed, then they can be approved or denied before it can be used."
+ "businessWhitelistDescription": "The business system will be whitelisted. Any new business must first be reviewed, then they can be approved or denied before it can be used.",
+ "timeZone": "Time Zone",
+ "timeZoneDescription": "The time zone that will be used in the SnailyCAD instance. This will apply to all users."
},
"DiscordRolesTab": {
"discordRoles": "Discord Roles",
diff --git a/apps/client/src/components/admin/manage/cad-settings/general/general-settings-tab.tsx b/apps/client/src/components/admin/manage/cad-settings/general/general-settings-tab.tsx
index 5b2f2f438..e743cda10 100644
--- a/apps/client/src/components/admin/manage/cad-settings/general/general-settings-tab.tsx
+++ b/apps/client/src/components/admin/manage/cad-settings/general/general-settings-tab.tsx
@@ -2,7 +2,15 @@ import * as React from "react";
import { useAuth } from "context/AuthContext";
import { useTranslations } from "use-intl";
import useFetch from "lib/useFetch";
-import { Button, Input, Loader, SwitchField, TextField, Textarea } from "@snailycad/ui";
+import {
+ Button,
+ Input,
+ Loader,
+ SelectField,
+ SwitchField,
+ TextField,
+ Textarea,
+} from "@snailycad/ui";
import { handleValidate } from "lib/handleValidate";
import { CAD_SETTINGS_SCHEMA } from "@snailycad/schemas";
import { ImageSelectInput, validateFile } from "components/form/inputs/ImageSelectInput";
@@ -14,6 +22,8 @@ import type { PutCADSettingsData } from "@snailycad/types/api";
import { TabsContent } from "@radix-ui/react-tabs";
import { SettingsTabs } from "components/admin/cad-settings/layout";
+import timeZones from "./timezones.json";
+
export function GeneralSettingsTab() {
const [logo, setLogo] = React.useState<(File | string) | null>(null);
const [headerId, setHeaderId] = React.useState<(File | string) | null>(null);
@@ -111,6 +121,7 @@ export function GeneralSettingsTab() {
registrationCode: cad.registrationCode ?? "",
roleplayEnabled: cad.miscCadSettings?.roleplayEnabled ?? true,
cadOGDescription: cad.miscCadSettings?.cadOGDescription ?? "",
+ timeZone: cad?.timeZone ?? null,
};
return (
@@ -146,6 +157,24 @@ export function GeneralSettingsTab() {
/>
+
+ ({
+ label: tz,
+ value: tz,
+ }))}
+ selectedKey={values.timeZone}
+ onSelectionChange={(value) => setFieldValue("timeZone", value)}
+ />
+
+
{AOP ? (
(null);
const isMounted = useMounted();
+ const { dateTime } = useFormatter();
+ const timeZone = useTimeZone();
React.useEffect(() => {
function setTime() {
if (ref.current && isMounted) {
- ref.current.textContent = format(new Date(), "HH:mm:ss - yyyy-MM-dd");
+ const time = dateTime(Date.now(), {
+ timeZone,
+ dateStyle: "medium",
+ timeStyle: "medium",
+ });
+
+ ref.current.textContent = `${time} ${timeZone ? `(${timeZone})` : ""}`;
}
}
setTime();
- const interval = setInterval(setTime);
+ const interval = setInterval(setTime, 1_000);
return () => {
clearInterval(interval);
};
- }, [isMounted]);
+ }, [isMounted, timeZone]); // eslint-disable-line react-hooks/exhaustive-deps
return ref;
}
diff --git a/apps/client/src/pages/_app.tsx b/apps/client/src/pages/_app.tsx
index 8a341031f..b89bd4c32 100644
--- a/apps/client/src/pages/_app.tsx
+++ b/apps/client/src/pages/_app.tsx
@@ -36,6 +36,8 @@ export default function App({ Component, router, pageProps, ...rest }: AppProps)
const url = `${protocol}//${host}`;
const user = pageProps.session as User | null;
const locale = user?.locale ?? router.locale ?? "en";
+ const cad = pageProps.cad ?? pageProps.session?.cad ?? null;
+ const timeZone = cad?.timeZone ?? undefined;
React.useEffect(() => {
const handleRouteStart = async () => {
@@ -82,6 +84,7 @@ export default function App({ Component, router, pageProps, ...rest }: AppProps)
defaultTranslationValues={{
span: (children) => {children},
}}
+ timeZone={timeZone}
onError={console.warn}
locale={locale}
messages={pageProps.messages}
diff --git a/packages/schemas/src/admin/index.ts b/packages/schemas/src/admin/index.ts
index 743088c99..8126d0f44 100644
--- a/packages/schemas/src/admin/index.ts
+++ b/packages/schemas/src/admin/index.ts
@@ -11,6 +11,7 @@ export const CAD_SETTINGS_SCHEMA = z.object({
registrationCode: z.string().max(255).optional(),
businessWhitelisted: z.boolean().optional(),
image: z.any().nullish(),
+ timeZone: z.string().nullish(),
});
export const LIVE_MAP_SETTINGS = z.object({
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index 53e9a3d73..44f7488d0 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -26,7 +26,8 @@ type CADPick =
| "discordRoles"
| "discordRolesId"
| "version"
- | "autoSetUserPropertiesId";
+ | "autoSetUserPropertiesId"
+ | "timeZone";
export type cad = Pick<
Omit & {
diff --git a/packages/ui/src/components/full-date.tsx b/packages/ui/src/components/full-date.tsx
index 6b2f4f578..680c301f1 100644
--- a/packages/ui/src/components/full-date.tsx
+++ b/packages/ui/src/components/full-date.tsx
@@ -1,5 +1,5 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../components/hover-card";
-import { useFormatter } from "use-intl";
+import { useFormatter, useTimeZone } from "use-intl";
interface FullDateProps {
children: Date | string | number;
@@ -10,6 +10,7 @@ interface FullDateProps {
export function FullDate({ children, onlyDate, relative, isDateOfBirth }: FullDateProps) {
const { dateTime, relativeTime } = useFormatter();
+ const timezone = useTimeZone();
const isCorrectDate = isValidDate(children);
if (!isCorrectDate) {
@@ -23,6 +24,7 @@ export function FullDate({ children, onlyDate, relative, isDateOfBirth }: FullDa
const relativeFormattedTime = relativeTime(date, new Date());
const formattedTime = dateTime(date, {
+ timeZone: timezone,
dateStyle: "medium",
timeStyle: onlyDate ? undefined : "medium",
});
@@ -38,7 +40,8 @@ export function FullDate({ children, onlyDate, relative, isDateOfBirth }: FullDa
- {dateTime(date, { dateStyle: "full", timeStyle: onlyDate ? undefined : "medium" })}
+ {dateTime(date, { dateStyle: "full", timeStyle: onlyDate ? undefined : "medium" })}{" "}
+ {timezone ? `(${timezone})` : ""}