diff --git a/components/Theme.tsx b/components/Theme.tsx
new file mode 100644
index 00000000..78350999
--- /dev/null
+++ b/components/Theme.tsx
@@ -0,0 +1,19 @@
+import {useEffect, useState} from "react";
+
+const Theme = (theme?: string) => {
+ const [isReady, setIsReady] = useState(false);
+ useEffect( () => {
+ if (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches) {
+ document.documentElement.classList.add("dark");
+ } else {
+ document.documentElement.classList.add(theme);
+ }
+ setIsReady(true);
+ }, []);
+
+ return {
+ isReady
+ }
+};
+
+export default Theme;
diff --git a/pages/[user].tsx b/pages/[user].tsx
index bdb78725..960e90a0 100644
--- a/pages/[user].tsx
+++ b/pages/[user].tsx
@@ -1,10 +1,14 @@
import { GetServerSideProps } from "next";
import Head from "next/head";
import Link from "next/link";
-import prisma from "../lib/prisma";
+import prisma, {whereAndSelect} from "@lib/prisma";
import Avatar from "../components/Avatar";
+import Theme from "@components/Theme";
export default function User(props): User {
+
+ const { isReady } = Theme(props.user.theme);
+
const eventTypes = props.eventTypes.map((type) => (
{props.user.name || props.user.username} | Calendso
@@ -50,21 +54,13 @@ export default function User(props): User {
}
export const getServerSideProps: GetServerSideProps = async (context) => {
- const user = await prisma.user.findFirst({
- where: {
- username: context.query.user.toLowerCase(),
- },
- select: {
- id: true,
- username: true,
- email: true,
- name: true,
- bio: true,
- avatar: true,
- eventTypes: true,
- },
- });
+ const user = await whereAndSelect(prisma.user.findFirst, {
+ username: context.query.user.toLowerCase(),
+ }, [
+ "id", "username", "email", "name", "bio", "avatar", "eventTypes", "theme"
+ ]
+ );
if (!user) {
return {
notFound: true,
diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx
index 52e0773b..d8ba07bf 100644
--- a/pages/[user]/[type].tsx
+++ b/pages/[user]/[type].tsx
@@ -13,12 +13,15 @@ import Avatar from "../../components/Avatar";
import { timeZone } from "../../lib/clock";
import DatePicker from "../../components/booking/DatePicker";
import PoweredByCalendso from "../../components/ui/PoweredByCalendso";
+import Theme from "@components/Theme";
export default function Type(props): Type {
// Get router variables
const router = useRouter();
const { rescheduleUid } = router.query;
+ const { isReady } = Theme(props.user.theme);
+
const [selectedDate, setSelectedDate] = useState
();
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const [timeFormat, setTimeFormat] = useState("h:mma");
@@ -44,7 +47,7 @@ export default function Type(props): Type {
setTimeFormat(is24hClock ? "HH:mm" : "h:mma");
};
- return (
+ return isReady && (
@@ -174,6 +177,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
"weekStart",
"availability",
"hideBranding",
+ "theme",
]
);
diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx
index ec348b69..fe305e2c 100644
--- a/pages/[user]/book.tsx
+++ b/pages/[user]/book.tsx
@@ -2,7 +2,7 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
-import prisma from "../../lib/prisma";
+import prisma, {whereAndSelect} from "../../lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
@@ -14,6 +14,7 @@ import { LocationType } from "../../lib/location";
import Avatar from "../../components/Avatar";
import Button from "../../components/ui/Button";
import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
+import Theme from "@components/Theme";
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -32,7 +33,10 @@ export default function Book(props: any): JSX.Element {
const [selectedLocation, setSelectedLocation] = useState(
locations.length === 1 ? locations[0].type : ""
);
+
+ const { isReady } = Theme(props.user.theme);
const telemetry = useTelemetry();
+
useEffect(() => {
setPreferredTimeZone(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess());
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
@@ -137,7 +141,7 @@ export default function Book(props: any): JSX.Element {
book();
};
- return (
+ return isReady && (
@@ -366,19 +370,19 @@ export default function Book(props: any): JSX.Element {
}
export async function getServerSideProps(context) {
- const user = await prisma.user.findFirst({
- where: {
+
+ const user = await whereAndSelect(prisma.user.findFirst, {
username: context.query.user,
- },
- select: {
- username: true,
- name: true,
- email: true,
- bio: true,
- avatar: true,
- eventTypes: true,
- },
- });
+ }, [
+ "username",
+ "name",
+ "email",
+ "bio",
+ "avatar",
+ "eventTypes",
+ "theme",
+ ]
+ );
const eventType = await prisma.eventType.findUnique({
where: {
diff --git a/pages/api/user/profile.ts b/pages/api/user/profile.ts
index dfb13315..0cfcab43 100644
--- a/pages/api/user/profile.ts
+++ b/pages/api/user/profile.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from 'next-auth/client';
-import prisma from '../../../lib/prisma';
+import prisma, {whereAndSelect} from '@lib/prisma';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({req: req});
@@ -11,15 +11,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
// Get user
- const user = await prisma.user.findUnique({
- where: {
- email: session.user.email,
+ const user = await whereAndSelect(prisma.user.findUnique, {
+ id: session.user.id,
},
- select: {
- id: true,
- password: true
- }
- });
+ [ "id", "password" ]
+ );
if (!user) { res.status(404).json({message: 'User not found'}); return; }
@@ -42,6 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const timeZone = req.body.timeZone;
const weekStart = req.body.weekStart;
const hideBranding = req.body.hideBranding;
+ const theme = req.body.theme;
const updateUser = await prisma.user.update({
where: {
@@ -52,11 +49,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name,
avatar,
bio: description,
- timeZone: timeZone,
- weekStart: weekStart,
- hideBranding: hideBranding,
+ timeZone,
+ weekStart,
+ hideBranding,
+ theme,
},
});
return res.status(200).json({message: 'Profile updated successfully'});
-}
\ No newline at end of file
+}
diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx
index 8278a55c..e103b61c 100644
--- a/pages/settings/profile.tsx
+++ b/pages/settings/profile.tsx
@@ -1,12 +1,13 @@
import { GetServerSideProps } from "next";
import Head from "next/head";
-import { useRef, useState } from "react";
-import prisma from "../../lib/prisma";
+import {useEffect, useRef, useState} from "react";
+import prisma, {whereAndSelect} from "@lib/prisma";
import Modal from "../../components/Modal";
import Shell from "../../components/Shell";
import SettingsShell from "../../components/Settings";
import Avatar from "../../components/Avatar";
import { getSession } from "next-auth/client";
+import Select from "react-select";
import TimezoneSelect from "react-timezone-select";
import { UsernameInput } from "../../components/ui/UsernameInput";
import ErrorAlert from "../../components/ui/alerts/Error";
@@ -18,12 +19,23 @@ export default function Settings(props) {
const descriptionRef = useRef();
const avatarRef = useRef();
const hideBrandingRef = useRef();
+ const [selectedTheme, setSelectedTheme] = useState({ value: "" });
const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone });
- const [selectedWeekStartDay, setSelectedWeekStartDay] = useState(props.user.weekStart || "Sunday");
+ const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: "" });
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
+ const themeOptions = [
+ {value: 'light', label: 'Light'},
+ {value: 'dark', label: 'Dark'}
+ ];
+
+ useEffect( () => {
+ setSelectedTheme(props.user.theme ? themeOptions.find( (theme) => theme.value === props.user.theme ) : null);
+ setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart });
+ }, []);
+
const closeSuccessModal = () => {
setSuccessModalOpen(false);
};
@@ -43,7 +55,7 @@ export default function Settings(props) {
const enteredDescription = descriptionRef.current.value;
const enteredAvatar = avatarRef.current.value;
const enteredTimeZone = selectedTimeZone.value;
- const enteredWeekStartDay = selectedWeekStartDay;
+ const enteredWeekStartDay = selectedWeekStartDay.value;
const enteredHideBranding = hideBrandingRef.current.checked;
// TODO: Add validation
@@ -58,6 +70,7 @@ export default function Settings(props) {
timeZone: enteredTimeZone,
weekStart: enteredWeekStartDay,
hideBranding: enteredHideBranding,
+ theme: selectedTheme ? selectedTheme.value : null,
}),
headers: {
"Content-Type": "application/json",
@@ -124,8 +137,8 @@ export default function Settings(props) {
name="about"
placeholder="A little something about yourself."
rows={3}
+ defaultValue={props.user.bio}
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
- {props.user.bio}
@@ -147,14 +160,46 @@ export default function Settings(props) {
First Day of Week
-
+ onChange={setSelectedWeekStartDay}
+ className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
+ options={[
+ { value:'Sunday', label:'Sunday' },
+ { value:'Monday', label:'Monday' }
+ ]} />
+
+
+
@@ -257,22 +302,20 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
- const user = await prisma.user.findFirst({
- where: {
- email: session.user.email,
- },
- select: {
- id: true,
- username: true,
- name: true,
- email: true,
- bio: true,
- avatar: true,
- timeZone: true,
- weekStart: true,
- hideBranding: true,
- },
- });
+ const user = await whereAndSelect(prisma.user.findFirst, {
+ id: session.user.id,
+ }, [
+ "id",
+ "username",
+ "name",
+ "email",
+ "bio",
+ "avatar",
+ "timeZone",
+ "weekStart",
+ "hideBranding",
+ "theme"
+ ]);
return {
props: { user }, // will be passed to the page component as props
diff --git a/pages/success.tsx b/pages/success.tsx
index 286897b9..20d9e7c3 100644
--- a/pages/success.tsx
+++ b/pages/success.tsx
@@ -1,6 +1,6 @@
import Head from "next/head";
import Link from "next/link";
-import prisma from "../lib/prisma";
+import prisma, {whereAndSelect} from "../lib/prisma";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { CheckIcon } from "@heroicons/react/outline";
@@ -11,6 +11,7 @@ import toArray from "dayjs/plugin/toArray";
import timezone from "dayjs/plugin/timezone";
import { createEvent } from "ics";
import { getEventName } from "../lib/event";
+import Theme from "@components/Theme";
dayjs.extend(utc);
dayjs.extend(toArray);
@@ -22,6 +23,7 @@ export default function Success(props) {
const [is24h, setIs24h] = useState(false);
const [date, setDate] = useState(dayjs.utc(router.query.date));
+ const { isReady } = Theme(props.user.theme);
useEffect(() => {
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
@@ -56,7 +58,7 @@ export default function Success(props) {
return encodeURIComponent(event.value);
}
- return (
+ return isReady && (
Booking Confirmed | {eventName} | Calendso
@@ -212,32 +214,26 @@ export default function Success(props) {
}
export async function getServerSideProps(context) {
- const user = await prisma.user.findFirst({
- where: {
- username: context.query.user,
- },
- select: {
- username: true,
- name: true,
- bio: true,
- avatar: true,
- eventTypes: true,
- hideBranding: true,
- },
- });
- const eventType = await prisma.eventType.findUnique({
- where: {
+ const user = (context.query.user) ? await whereAndSelect(prisma.user.findFirst, {
+ username: context.query.user,
+ }, [
+ "username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme"
+ ]
+ ) : null;
+
+ if (!user) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const eventType = await whereAndSelect(prisma.eventType.findUnique, {
id: parseInt(context.query.type),
- },
- select: {
- id: true,
- title: true,
- description: true,
- length: true,
- eventName: true,
- },
- });
+ }, [
+ "id", "title", "description", "length", "eventName"
+ ]
+ );
return {
props: {
diff --git a/prisma/migrations/20210709231256_add_user_theme/migration.sql b/prisma/migrations/20210709231256_add_user_theme/migration.sql
new file mode 100644
index 00000000..75875267
--- /dev/null
+++ b/prisma/migrations/20210709231256_add_user_theme/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "theme" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ddaf7d81..f0838096 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -50,6 +50,7 @@ model User {
endTime Int @default(1440)
bufferTime Int @default(0)
hideBranding Boolean @default(false)
+ theme String?
createdDate DateTime @default(now()) @map(name: "created")
eventTypes EventType[]
credentials Credential[]
diff --git a/tailwind.config.js b/tailwind.config.js
index 24bfaf0f..c14305a9 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,7 +1,7 @@
module.exports = {
mode: "jit",
purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
- darkMode: "media",
+ darkMode: "class",
theme: {
extend: {
colors: {