Implemented theme through user preferences
This commit is contained in:
parent
36b258f4b7
commit
5206fb4f88
10 changed files with 161 additions and 98 deletions
19
components/Theme.tsx
Normal file
19
components/Theme.tsx
Normal file
|
@ -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;
|
|
@ -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) => (
|
||||
<li
|
||||
key={type.id}
|
||||
|
@ -20,7 +24,7 @@ export default function User(props): User {
|
|||
</Link>
|
||||
</li>
|
||||
));
|
||||
return (
|
||||
return isReady && (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{props.user.name || props.user.username} | Calendso</title>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<Dayjs>();
|
||||
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 && (
|
||||
<div>
|
||||
<Head>
|
||||
<title>
|
||||
|
@ -174,6 +177,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
"weekStart",
|
||||
"availability",
|
||||
"hideBranding",
|
||||
"theme",
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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<LocationType>(
|
||||
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 && (
|
||||
<div>
|
||||
<Head>
|
||||
<title>
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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,9 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
name,
|
||||
avatar,
|
||||
bio: description,
|
||||
timeZone: timeZone,
|
||||
weekStart: weekStart,
|
||||
hideBranding: hideBranding,
|
||||
timeZone,
|
||||
weekStart,
|
||||
hideBranding,
|
||||
theme,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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<HTMLTextAreaElement>();
|
||||
const avatarRef = useRef<HTMLInputElement>();
|
||||
const hideBrandingRef = useRef<HTMLInputElement>();
|
||||
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}
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -147,14 +160,46 @@ export default function Settings(props) {
|
|||
First Day of Week
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<select
|
||||
<Select
|
||||
id="weekStart"
|
||||
value={selectedWeekStartDay}
|
||||
onChange={(e) => setSelectedWeekStartDay(e.target.value)}
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md">
|
||||
<option value="Sunday">Sunday</option>
|
||||
<option value="Monday">Monday</option>
|
||||
</select>
|
||||
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' }
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="theme" className="block text-sm font-medium text-gray-700">
|
||||
Single Theme
|
||||
</label>
|
||||
<div className="my-1">
|
||||
<Select
|
||||
id="theme"
|
||||
isDisabled={!selectedTheme}
|
||||
defaultValue={selectedTheme || themeOptions[0]}
|
||||
onChange={setSelectedTheme}
|
||||
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={themeOptions} />
|
||||
</div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="theme-adjust-os"
|
||||
name="theme-adjust-os"
|
||||
type="checkbox"
|
||||
onChange={(e) => setSelectedTheme(e.target.checked ? null : themeOptions[0])}
|
||||
defaultChecked={!selectedTheme}
|
||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="theme-adjust-os" className="font-medium text-gray-700">
|
||||
Automatically adjust theme based on invitee preferences
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -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
|
||||
|
|
|
@ -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 && (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Booking Confirmed | {eventName} | Calendso</title>
|
||||
|
@ -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: {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "theme" TEXT;
|
|
@ -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[]
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in a new issue