Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commite678cc1

Browse files
committed
Add calendar weekly view enhancements and welcome modal feature
1 parentfbe0a32 commite678cc1

File tree

7 files changed

+264
-3
lines changed

7 files changed

+264
-3
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import{useSession}from"next-auth/react";
2+
import{useMemo,useEffect}from"react";
3+
4+
importdayjsfrom"@calcom/dayjs";
5+
importtype{CalendarEvent}from"@calcom/features/calendars/weeklyview/types/events";
6+
import{BookingStatus}from"@calcom/prisma/enums";
7+
import{trpc}from"@calcom/trpc/react";
8+
9+
typeUseOnboardingCalendarEventsProps={
10+
startDate:Date;
11+
endDate:Date;
12+
};
13+
14+
exportconstuseOnboardingCalendarEvents=({ startDate, endDate}:UseOnboardingCalendarEventsProps)=>{
15+
const{data:session}=useSession();
16+
constutils=trpc.useUtils();
17+
18+
// Watch for calendar installations to refetch events
19+
const{data:connectedCalendars}=trpc.viewer.calendars.connectedCalendars.useQuery(undefined,{
20+
refetchInterval:5000,// Poll every 5 seconds to detect new calendar installations
21+
});
22+
23+
const{data:busyEvents}=trpc.viewer.availability.user.useQuery(
24+
{
25+
username:session?.user?.username||"",
26+
dateFrom:dayjs(startDate).startOf("day").utc().format(),
27+
dateTo:dayjs(endDate).endOf("day").utc().format(),
28+
withSource:true,
29+
},
30+
{
31+
enabled:!!session?.user?.username,
32+
// Don't show loading state - return empty array immediately
33+
placeholderData:()=>({busy:[]}),
34+
}
35+
);
36+
37+
// Refetch availability when calendars change
38+
useEffect(()=>{
39+
if(connectedCalendars?.connectedCalendars?.length){
40+
utils.viewer.availability.user.invalidate();
41+
}
42+
},[connectedCalendars?.connectedCalendars?.length,utils.viewer.availability.user]);
43+
44+
// Format events similar to Troubleshooter
45+
// Always return an array, never undefined
46+
constevents=useMemo(():CalendarEvent[]=>{
47+
if(!busyEvents?.busy)return[];
48+
49+
returnbusyEvents.busy.map((event,idx)=>{
50+
return{
51+
id:idx,
52+
title:event.title??`Busy`,
53+
start:newDate(event.start),
54+
end:newDate(event.end),
55+
options:{
56+
color:event.source ?undefined :undefined,
57+
status:BookingStatus.ACCEPTED,
58+
},
59+
};
60+
});
61+
},[busyEvents]);
62+
63+
return{
64+
events,
65+
isLoading:false,// Never show loading state
66+
};
67+
};

‎packages/features/calendars/weeklyview/components/DateValues/index.tsx‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dayjs from "@calcom/dayjs";
44
import{useLocale}from"@calcom/lib/hooks/useLocale";
55
importclassNamesfrom"@calcom/ui/classNames";
66

7+
import{useCalendarStore}from"../../state/store";
78
importtype{BorderColor}from"../../types/common";
89

910
typeProps={
@@ -15,9 +16,38 @@ type Props = {
1516

1617
exportfunctionDateValues({ showBorder, borderColor, days, containerNavRef}:Props){
1718
const{ i18n}=useLocale();
19+
consttimezone=useCalendarStore((state)=>state.timezone);
20+
constshowTimezone=useCalendarStore((state)=>state.showTimezone??false);
21+
1822
constformatDate=(date:dayjs.Dayjs):string=>{
1923
returnnewIntl.DateTimeFormat(i18n.language,{weekday:"short"}).format(date.toDate());
2024
};
25+
26+
constgetTimezoneDisplay=()=>{
27+
if(!showTimezone||!timezone)returnnull;
28+
try{
29+
consttimeRaw=dayjs().tz(timezone);
30+
constutcOffsetInMinutes=timeRaw.utcOffset();
31+
32+
// Convert offset to decimal hours
33+
constoffsetInHours=Math.abs(utcOffsetInMinutes/60);
34+
constsign=utcOffsetInMinutes<0 ?"-" :"+";
35+
36+
// If offset is 0, just return "GMT"
37+
if(utcOffsetInMinutes===0){
38+
return"GMT";
39+
}
40+
41+
// Format as decimal (e.g., 1.5 for 1:30, 1 for 1:00)
42+
constoffsetFormatted=`${sign}${offsetInHours}`;
43+
44+
return`GMT${offsetFormatted}`;
45+
}catch{
46+
// Fallback to showing the timezone name if formatting fails
47+
returntimezone.split("/").pop()?.replace(/_/g," ")||timezone;
48+
}
49+
};
50+
2151
return(
2252
<div
2353
ref={containerNavRef}
@@ -49,11 +79,14 @@ export function DateValues({ showBorder, borderColor, days, containerNavRef }: P
4979
<divclassName="text-subtle -mr-px hidden auto-cols-fr leading-6 sm:flex">
5080
<div
5181
className={classNames(
52-
"col-end-1 w-16",
82+
"col-end-1flexw-16 items-center justify-center",
5383
showBorder&&
5484
(borderColor==="subtle" ?"border-l-subtle border-l" :"border-l-default border-l")
85+
)}>
86+
{showTimezone&&timezone&&(
87+
<spanclassName="text-muted text-xs font-medium">{getTimezoneDisplay()}</span>
5588
)}
56-
/>
89+
</div>
5790
{days.map((day)=>{
5891
constisToday=dayjs().isSame(day,"day");
5992
return(

‎packages/features/calendars/weeklyview/state/store.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const defaultState: CalendarComponentProps = {
2525
showBackgroundPattern:true,
2626
showBorder:true,
2727
borderColor:"default",
28+
showTimezone:false,
2829
};
2930

3031
exportfunctioncreateCalendarStore(initial?:Partial<CalendarComponentProps>):StoreApi<CalendarStoreProps>{

‎packages/features/calendars/weeklyview/types/state.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ export type CalendarState = {
147147
*@default "default"
148148
*/
149149
borderColor?:BorderColor;
150+
/**
151+
* Show the timezone in the empty space next to the date headers
152+
*@default false
153+
*/
154+
showTimezone?:boolean;
150155
};
151156

152157
exporttypeCalendarComponentProps=CalendarPublicActions&CalendarState&{isPending?:boolean};

‎packages/features/shell/DynamicModals.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import{WelcomeToOrganizationsModal}from"@calcom/features/ee/organizations/components/WelcomeToOrganizationsModal";
44

5+
import{WelcomeToCalcomModal}from"./components/WelcomeToCalcomModal";
6+
57
/**
68
* Container for all query-param driven modals that should appear globally across the app.
79
* This keeps the Shell component clean and provides a centralized place for dynamic modals.
8-
*
10+
*
911
* We can probably also use this for thinks like the T&C and Privacy Policy modals. That @marketing are discussing
1012
*
1113
* To add a new modal:
@@ -16,6 +18,7 @@ export function DynamicModals() {
1618
return(
1719
<>
1820
<WelcomeToOrganizationsModal/>
21+
<WelcomeToCalcomModal/>
1922
{/* Add more query-param driven modals here */}
2023
</>
2124
);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client";
2+
3+
import{APP_NAME}from"@calcom/lib/constants";
4+
import{useLocale}from"@calcom/lib/hooks/useLocale";
5+
import{Button}from"@calcom/ui/components/button";
6+
import{Dialog,DialogContent}from"@calcom/ui/components/dialog";
7+
import{Icon}from"@calcom/ui/components/icon";
8+
import{Logo}from"@calcom/ui/components/logo";
9+
10+
import{useWelcomeToCalcomModal}from"../hooks/useWelcomeToCalcomModal";
11+
12+
constfeatures=["1_user","unlimited_calendars","accept_payments_via_stripe"];
13+
14+
exportfunctionWelcomeToCalcomModal(){
15+
const{ t}=useLocale();
16+
const{ isOpen, closeModal}=useWelcomeToCalcomModal();
17+
18+
constLARGE={outer:48,icon:24};
19+
constRINGS=[60,95,130];// Ring radii in px
20+
constRING_STROKE=1;
21+
22+
return(
23+
<Dialogopen={isOpen}onOpenChange={(open)=>!open&&closeModal()}>
24+
<DialogContentsize="default"className="!p-0">
25+
<divclassName="flex flex-col gap-4 p-6">
26+
<divclassName="flex flex-col items-center gap-1">
27+
<LogoclassName="h-10 w-auto"/>
28+
</div>
29+
30+
{/* User illustration with rings */}
31+
<div
32+
className="relative mx-auto"
33+
style={{
34+
width:320,
35+
height:220,
36+
maskImage:"radial-gradient(ellipse 100% 60% at center, black 30%, transparent 85%)",
37+
WebkitMaskImage:"radial-gradient(ellipse 100% 60% at center, black 30%, transparent 85%)",
38+
}}>
39+
{/* Center origin */}
40+
<divclassName="absolute left-1/2 top-1/2"style={{transform:"translate(-50%, -50%)"}}>
41+
{/* Rings */}
42+
{RINGS.map((r,i)=>(
43+
<div
44+
key={i}
45+
className="pointer-events-none absolute rounded-full border"
46+
style={{
47+
width:2*r,
48+
height:2*r,
49+
left:`calc(50% -${r}px)`,
50+
top:`calc(50% -${r}px)`,
51+
borderWidth:RING_STROKE,
52+
borderColor:"var(--cal-border-subtle)",
53+
}}
54+
/>
55+
))}
56+
57+
{/* Central user icon */}
58+
<div
59+
className="from-default to-muted border-subtle absolute flex items-center justify-center rounded-full border bg-gradient-to-b shadow-sm"
60+
style={{
61+
width:LARGE.outer,
62+
height:LARGE.outer,
63+
left:"50%",
64+
top:"50%",
65+
transform:"translate(-50%, -50%)",
66+
}}>
67+
<Icon
68+
name="user"
69+
className="text-emphasis opacity-70"
70+
style={{width:LARGE.icon,height:LARGE.icon}}
71+
/>
72+
</div>
73+
</div>
74+
</div>
75+
76+
<divclassName="mb-2 flex flex-col gap-2 text-center">
77+
<h2className="font-cal text-emphasis text-2xl leading-none">
78+
{t("welcome_to_calcom",{appName:APP_NAME})}
79+
</h2>
80+
<pclassName="text-default text-sm leading-normal">{t("personal_welcome_description")}</p>
81+
</div>
82+
83+
<divclassName="mb-2 flex flex-col gap-3">
84+
{features.map((feature)=>(
85+
<divkey={feature}className="flex items-start gap-2">
86+
<Iconname="check"className="text-muted mt-0.5 h-4 w-4 flex-shrink-0"/>
87+
<spanclassName="text-default text-sm font-medium leading-tight">{t(feature)}</span>
88+
</div>
89+
))}
90+
</div>
91+
</div>
92+
93+
<divclassName="bg-muted border-subtle mt-6 flex items-center justify-between rounded-b-2xl border-t px-8 py-6">
94+
<Button
95+
color="minimal"
96+
href="https://cal.com/docs"
97+
target="_blank"
98+
EndIcon="external-link"
99+
className="pointer-events-none opacity-0">
100+
{t("learn_more")}
101+
</Button>
102+
<Buttoncolor="primary"onClick={closeModal}>
103+
{t("continue")}
104+
</Button>
105+
</div>
106+
</DialogContent>
107+
</Dialog>
108+
);
109+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import{parseAsBoolean,useQueryState}from"nuqs";
2+
import{useEffect,useState}from"react";
3+
4+
import{sessionStorage}from"@calcom/lib/webstorage";
5+
6+
constSTORAGE_KEY="showWelcomeToCalcomModal";
7+
8+
exportfunctionuseWelcomeToCalcomModal(){
9+
const[welcomeToCalcomModal,setWelcomeToCalcomModal]=useQueryState(
10+
"welcomeToCalcomModal",
11+
parseAsBoolean.withDefault(false)
12+
);
13+
14+
const[isOpen,setIsOpen]=useState(false);
15+
16+
useEffect(()=>{
17+
if(welcomeToCalcomModal){
18+
setIsOpen(true);
19+
return;
20+
}
21+
22+
if(sessionStorage.getItem(STORAGE_KEY)==="true"){
23+
setIsOpen(true);
24+
}
25+
},[welcomeToCalcomModal]);
26+
27+
constcloseModal=()=>{
28+
setIsOpen(false);
29+
// Remove the query param from URL
30+
setWelcomeToCalcomModal(null);
31+
// Also clear sessionStorage
32+
sessionStorage.removeItem(STORAGE_KEY);
33+
};
34+
35+
return{
36+
isOpen,
37+
closeModal,
38+
};
39+
}
40+
41+
exportfunctionsetShowWelcomeToCalcomModalFlag(){
42+
sessionStorage.setItem(STORAGE_KEY,"true");
43+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp