diff --git a/components/Theme.tsx b/components/Theme.tsx new file mode 100644 index 00000000..2c0dc1ca --- /dev/null +++ b/components/Theme.tsx @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function 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, + }; +} diff --git a/pages/[user].tsx b/pages/[user].tsx index bdb78725..71c311ce 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,10 +1,13 @@ 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) => (
  • )); return ( -
    - - {props.user.name || props.user.username} | Calendso - - + isReady && ( +
    + + {props.user.name || props.user.username} | Calendso + + -
    -
    - -

    - {props.user.name || props.user.username} -

    -

    {props.user.bio}

    -
    -
    -
      {eventTypes}
    - {eventTypes.length == 0 && ( -
    -

    Uh oh!

    -

    This user hasn't set up any event types yet.

    -
    - )} -
    -
    -
    +
    +
    + +

    + {props.user.name || props.user.username} +

    +

    {props.user.bio}

    +
    +
    +
      {eventTypes}
    + {eventTypes.length == 0 && ( +
    +

    Uh oh!

    +

    This user hasn't set up any event types yet.

    +
    + )} +
    +
    +
    + ) ); } export const getServerSideProps: GetServerSideProps = async (context) => { - const user = await prisma.user.findFirst({ - where: { + const user = await whereAndSelect( + prisma.user.findFirst, + { username: context.query.user.toLowerCase(), }, - select: { - id: true, - username: true, - email: true, - name: true, - bio: true, - avatar: true, - eventTypes: true, - }, - }); - + ["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 a2e9c82b..9716a3d5 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"); @@ -46,112 +49,114 @@ export default function Type(props): Type { }; return ( -
    - - - {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username} | - Calendso - - - + isReady && ( +
    + + + {rescheduleUid && "Reschedule"} {props.eventType.title} | {props.user.name || props.user.username}{" "} + | Calendso + + + - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> + + + + + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + /> - - - - - " + props.eventType.description - ).replace(/'/g, "%27") + - ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + - encodeURIComponent(props.user.avatar) - } - /> - -
    -
    -
    -
    - -

    {props.user.name}

    -

    - {props.eventType.title} -

    -

    - - {props.eventType.length} minutes -

    - - {isTimeOptionsOpen && ( - + + + + " + props.eventType.description + ).replace(/'/g, "%27") + + ".png?md=1&images=https%3A%2F%2Fcalendso.com%2Fcalendso-logo-white.svg&images=" + + encodeURIComponent(props.user.avatar) + } + /> + +
    +
    +
    +
    + +

    {props.user.name}

    +

    + {props.eventType.title} +

    +

    + + {props.eventType.length} minutes +

    + + {isTimeOptionsOpen && ( + + )} +

    {props.eventType.description}

    +
    + + {selectedDate && ( + )} -

    {props.eventType.description}

    - - {selectedDate && ( - - )}
    -
    - {!props.user.hideBranding && } -
    -
    + {!props.user.hideBranding && } + +
    + ) ); } @@ -175,6 +180,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..c03b86da 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")); @@ -138,247 +142,247 @@ export default function Book(props: any): JSX.Element { }; return ( -
    - - - {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} - {props.user.name || props.user.username} | Calendso - - - + isReady && ( +
    + + + {rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with{" "} + {props.user.name || props.user.username} | Calendso + + + -
    -
    -
    -
    - -

    {props.user.name}

    -

    - {props.eventType.title} -

    -

    - - {props.eventType.length} minutes -

    - {selectedLocation === LocationType.InPerson && ( +
    +
    +
    +
    + +

    {props.user.name}

    +

    + {props.eventType.title} +

    - - {locationInfo(selectedLocation).address} + + {props.eventType.length} minutes

    - )} -

    - - {preferredTimeZone && - dayjs(date) - .tz(preferredTimeZone) - .format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} -

    -

    {props.eventType.description}

    -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    - {locations.length > 1 && ( -
    - Location - {locations.map((location) => ( - - ))} -
    + {selectedLocation === LocationType.InPerson && ( +

    + + {locationInfo(selectedLocation).address} +

    )} - {selectedLocation === LocationType.Phone && ( +

    + + {preferredTimeZone && + dayjs(date) + .tz(preferredTimeZone) + .format((is24h ? "H:mm" : "h:mma") + ", dddd DD MMMM YYYY")} +

    +

    {props.eventType.description}

    +
    +
    +
    -
    - )} - {props.eventType.customInputs && - props.eventType.customInputs - .sort((a, b) => a.id - b.id) - .map((input) => ( -
    - {input.type !== EventTypeCustomInputType.Bool && ( - - )} - {input.type === EventTypeCustomInputType.TextLong && ( - + 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">
    @@ -147,14 +161,48 @@ 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" }, + ]} + /> +
    +
    +
    + +
    + 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" + /> +
    +
    + +
    @@ -257,22 +305,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { redirect: { permanent: false, destination: "/auth/login" } }; } - const user = await prisma.user.findFirst({ - where: { - email: session.user.email, + const user = await whereAndSelect( + prisma.user.findFirst, + { + id: session.user.id, }, - select: { - id: true, - username: true, - name: true, - email: true, - bio: true, - avatar: true, - timeZone: true, - weekStart: true, - hideBranding: true, - }, - }); + ["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..9b08377e 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())); @@ -31,7 +33,7 @@ export default function Success(props) { const eventName = getEventName(name, props.eventType.title, props.eventType.eventName); function eventLink(): string { - let optional = {}; + const optional = {}; if (location) { optional["location"] = location; } @@ -57,187 +59,186 @@ export default function Success(props) { } return ( -
    - - Booking Confirmed | {eventName} | Calendso - - -
    -
    -
    -
    +
    + ) ); } 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 user = context.query.user + ? await whereAndSelect( + prisma.user.findFirst, + { + username: context.query.user, + }, + ["username", "name", "bio", "avatar", "eventTypes", "hideBranding", "theme"] + ) + : null; - const eventType = await prisma.eventType.findUnique({ - where: { + 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: {