Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion public/locales/en/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,8 @@
"updatePassword": "Update Password",
"resetPasswordSuccess": "Password Reset Successful",
"passwordResetSuccessfully": "Your password has been reset successfully.",
"resetPasswordErrorDesc": "An error occurred while resetting your password."
"resetPasswordErrorDesc": "An error occurred while resetting your password.",
"joiningChurch": "Joining church...",
"joinedChurch": "Successfully joined!",
"joinedChurchDesc": "Welcome to {{tenantName}}!"
}
1 change: 1 addition & 0 deletions public/locales/en/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"startsAt": "Starts at",
"endsAt": "Ends at",
"eventLinkText": "Event Link",
"addToGoogleCalendar": "Add to Google Calendar",
"private": "Private",
"fullDay": "Full Day"
}
10 changes: 10 additions & 0 deletions public/locales/en/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
"subtitle": "Subtitle",
"servicePersonnel": "Service Personnel",
"actions": "Actions",
"addToGoogleCalendar": "Add to Google Calendar",
"emptyValue": "—",
"email": "Email",
"notProvided": "Not provided",
"unknownUser": "Unknown user",
"unknownRole": "Unknown role",
"noAssignees": "Unassigned",
"loadServicePersonnelError": "Unable to load service personnel.",
"confirmDeleteScheduleTitle": "Confirm delete service schedule",
"confirmDeleteScheduleDescription": "Are you sure you want to delete the {{date}} {{serviceName}} service schedule? This action cannot be undone.",
"addServiceRoles": "Add Service Roles",
"roleName": "Role Name",
"roleNameRequired": "Role name is required",
Expand Down
5 changes: 4 additions & 1 deletion public/locales/zh-TW/auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,8 @@
"updatePassword": "更新密碼",
"resetPasswordSuccess": "密碼重設成功",
"passwordResetSuccessfully": "您的密碼已成功重設。",
"resetPasswordErrorDesc": "重設密碼時發生錯誤。"
"resetPasswordErrorDesc": "重設密碼時發生錯誤。",
"joiningChurch": "正在加入教會...",
"joinedChurch": "成功加入!",
"joinedChurchDesc": "歡迎來到{{tenantName}}!"
}
1 change: 1 addition & 0 deletions public/locales/zh-TW/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"startsAt": "開始於",
"endsAt": "結束於",
"eventLinkText": "活動連結",
"addToGoogleCalendar": "加入 Google 日曆",
"private": "私人",
"fullDay": "全天"
}
10 changes: 10 additions & 0 deletions public/locales/zh-TW/services.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@
"subtitle": "副標題",
"servicePersonnel": "服事人員",
"actions": "操作",
"addToGoogleCalendar": "加入 Google 日曆",
"emptyValue": "—",
"email": "電子郵件",
"notProvided": "未提供",
"unknownUser": "未知用戶",
"unknownRole": "未知角色",
"noAssignees": "未指派",
"loadServicePersonnelError": "無法載入服事人員資料",
"confirmDeleteScheduleTitle": "確認刪除服事排班",
"confirmDeleteScheduleDescription": "您確定要刪除 {{date}} 的 {{serviceName}} 服事排班嗎?此操作無法撤銷。",
"addServiceRoles": "新增服事角色",
"roleName": "角色名稱",
"roleNameRequired": "角色名稱為必填",
Expand Down
13 changes: 10 additions & 3 deletions src/components/Auth/GoogleOAuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ export function GoogleOAuthButton({ onSuccess: _onSuccess, onError }: GoogleOAut
const handleGoogleSignIn = async () => {
setIsLoading(true);
try {
// Get current redirect parameter if exists
// Get current flow and redirect parameters
const searchParams = new URLSearchParams(window.location.search);
const flowParam = searchParams.get("flow");
const redirectParam = searchParams.get("redirect");

// Build auth redirect URL
const authPath = slug ? `/tenant/${slug}/auth` : "/auth";
let redirectTo = window.location.origin + authPath;

// Append redirect parameter if it exists
// Preserve both flow and redirect parameters
const queryParams = new URLSearchParams();
if (flowParam) queryParams.set("flow", flowParam);
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flow parameter is now being preserved in the OAuth redirect, but there's no validation that the flow parameter is a valid AuthFlowStep value. If a malformed or invalid flow parameter is passed, it could cause unexpected behavior in the auth flow. Consider adding validation to ensure the flow parameter is one of the expected values before preserving it.

Suggested change
if (flowParam) queryParams.set("flow", flowParam);
if (flowParam && /^[a-zA-Z0-9_-]+$/.test(flowParam)) {
queryParams.set("flow", flowParam);
}

Copilot uses AI. Check for mistakes.
if (redirectParam && slug && redirectParam.startsWith(`/tenant/${slug}`)) {
redirectTo += `?redirect=${encodeURIComponent(redirectParam)}`;
queryParams.set("redirect", redirectParam);
}

if (queryParams.toString()) {
redirectTo += `?${queryParams.toString()}`;
}

const { data: _data, error } = await supabase.auth.signInWithOAuth({
Expand Down
4 changes: 3 additions & 1 deletion src/components/Events/EventActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface EventActionsProps {
onDeleteEvent: (eventId: string) => Promise<void>;
onCopyEvent?: (event: EventWithGroups) => void;
allGroups: Group[];
className?: string;
}

export function EventActions({
Expand All @@ -26,6 +27,7 @@ export function EventActions({
onDeleteEvent,
onCopyEvent,
allGroups,
className,
}: EventActionsProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
const [isDeleting, setIsDeleting] = React.useState(false);
Expand Down Expand Up @@ -74,7 +76,7 @@ export function EventActions({
};

return (
<div className="absolute top-4 right-4">
<div className={className}>
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
Expand Down
127 changes: 79 additions & 48 deletions src/components/Events/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { Badge } from "@/components/ui/badge";
import { EventActions } from "./EventActions";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { AddToGoogleCalendarLink } from "@/components/shared/AddToGoogleCalendarLink";
import { combineDateAndTimeLocal, parseISODateLocal } from "@/lib/googleCalendar";

interface EventCardProps {
event: EventWithGroups;
Expand Down Expand Up @@ -46,29 +48,60 @@ export function EventCard({
timeDisplay = `${t("events:endsAt")} ${event.end_time}`;
}

const calendarLabel = t("events:addToGoogleCalendar");
const hasAnyTime = !!event.start_time || !!event.end_time;
const calendarStart = hasAnyTime
? ((event.start_time && combineDateAndTimeLocal(event.date, event.start_time)) ??
parseISODateLocal(event.date) ??
new Date())
: (parseISODateLocal(event.date) ?? new Date());
const calendarEnd = hasAnyTime
? ((event.end_time && combineDateAndTimeLocal(event.date, event.end_time)) ??
new Date(calendarStart.getTime() + 60 * 60 * 1000))
: calendarStart;
Comment on lines +53 to +61
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calendar start/end date calculations are complex and harder to maintain inline. The nested ternary operators and fallback logic make the code difficult to read and reason about. Consider extracting this logic into a separate helper function or useMemo hook for better readability and testability.

Copilot uses AI. Check for mistakes.
const calendarDetails = [
event.description ?? "",
event.event_link ? `\n\n${event.event_link}` : "",
].join("");

return (
<Card className={cn("relative", event.visibility === "private" && "border-primary/30")}>
{isEditable && (
<EventActions
event={event}
onEventUpdated={onEventUpdated}
onDeleteEvent={onDeleteEvent}
onCopyEvent={onCopyEvent}
allGroups={allGroups}
/>
)}

<CardHeader>
<CardTitle>{event.name}</CardTitle>
<CardDescription className="flex items-center gap-1">
<Calendar className="h-4 w-4" /> {formattedDate}
{hasTime && (
<>
<span className="mx-1">•</span>
<Clock className="h-4 w-4" /> {timeDisplay}
</>
)}
</CardDescription>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<CardTitle className="truncate">{event.name}</CardTitle>
<CardDescription className="flex items-center gap-1">
<Calendar className="h-4 w-4" /> {formattedDate}
{hasTime && (
<>
<span className="mx-1">•</span>
<Clock className="h-4 w-4" /> {timeDisplay}
</>
)}
</CardDescription>
</div>
<div className="flex shrink-0 items-center gap-2">
<AddToGoogleCalendarLink
title={event.name}
start={calendarStart}
end={calendarEnd}
isAllDay={!hasAnyTime}
details={calendarDetails}
label={calendarLabel}
className="px-0"
/>
{isEditable && (
<EventActions
className="shrink-0"
event={event}
onEventUpdated={onEventUpdated}
onDeleteEvent={onDeleteEvent}
onCopyEvent={onCopyEvent}
allGroups={allGroups}
/>
)}
</div>
</div>
</CardHeader>

{event.description && (
Expand All @@ -77,36 +110,34 @@ export function EventCard({
</CardContent>
)}

{(event.event_link || (event.groups && event.groups.length > 0)) && (
<CardFooter className="flex flex-wrap gap-2">
{event.event_link && (
<a
href={event.event_link}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline hover:no-underline"
>
{t("events:eventLinkText")}
</a>
)}

{event.groups &&
event.groups.length > 0 &&
event.groups.map((group) =>
group ? (
<Badge key={group.id} variant="outline">
{group.name}
</Badge>
) : null,
)}
<CardFooter className="flex flex-wrap gap-2">
{event.event_link && (
<a
href={event.event_link}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary underline hover:no-underline"
>
{t("events:eventLinkText")}
</a>
)}

{event.visibility === "private" && (
<Badge variant="secondary" className="ml-auto">
{t("events:private")}
</Badge>
{event.groups &&
event.groups.length > 0 &&
event.groups.map((group) =>
group ? (
<Badge key={group.id} variant="outline">
{group.name}
</Badge>
) : null,
)}
</CardFooter>
)}

{event.visibility === "private" && (
<Badge variant="secondary" className="ml-auto">
{t("events:private")}
</Badge>
)}
</CardFooter>
Comment on lines +113 to +140
Copy link

Copilot AI Jan 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CardFooter is now always rendered even when it has no content. If event.event_link is null, event.groups is empty, and visibility is not "private", the CardFooter will render as an empty element. Consider conditionally rendering the CardFooter only when there's content to display, similar to the original code structure.

Copilot uses AI. Check for mistakes.
</Card>
);
}
Loading