Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
1 change: 0 additions & 1 deletion .claude/worktrees/sharp-lehmann
Submodule sharp-lehmann deleted from 0e187b
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@google/generative-ai": "^0.24.1",
"@mantine/carousel": "^8.3.10",
"@mantine/core": "^8.3.10",
"@mantine/dates": "^8.3.10",
Expand Down
23 changes: 23 additions & 0 deletions src/app/api/emoji-suggest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
import { NextRequest, NextResponse } from "next/server";

const genAI = new GoogleGenerativeAI(
process.env.GOOGLE_GENERATIVE_AI_API_KEY!
);

export async function POST(req: NextRequest) {
const { name } = await req.json();
if (!name?.trim()) return NextResponse.json({ emojis: [] });

Comment thread
guid3d marked this conversation as resolved.
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
const result = await model.generateContent(
Comment thread
guid3d marked this conversation as resolved.
Outdated
`Given the transaction name: "${name}", suggest 5 relevant emojis. Reply with ONLY the 5 emojis separated by spaces, nothing else. No words, no punctuation, just emojis.`
);
const text = result.response.text().trim();
const emojis = [...new Intl.Segmenter().segment(text)]
.map((s) => s.segment)
.filter((s) => /\p{Emoji}/u.test(s) && s !== " ")
.slice(0, 5);

return NextResponse.json({ emojis });
}
4 changes: 2 additions & 2 deletions src/app/group/[groupId]/components/TabTransactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Text,
Title,
rem,
useMantineColorScheme,
useComputedColorScheme,
} from "@mantine/core";
import { TotalSpendData } from "@/types";
import UserAvatar from "@/components/UserAvatar";
Expand All @@ -26,7 +26,7 @@ type TabTransactionsProps = {
};

const TabTransactions = ({ groupData, localUserId }: TabTransactionsProps) => {
const { colorScheme } = useMantineColorScheme();
const colorScheme = useComputedColorScheme();
const router = useRouter();
const { groupId } = useParams<{ groupId: string }>();
const { data, isPending, error } = useTransactions(groupId);
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
/>
</head>
<body>
<MantineProvider theme={theme}>
<MantineProvider theme={theme} defaultColorScheme="auto">
<ReactQueryClientProvider>{children}</ReactQueryClientProvider>
</MantineProvider>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const PageSetDetails = ({ form }: PageSetDetailsProps) => {
</Text>
</Center>
<Center>
<EmojiActionButtion form={form} />
<EmojiActionButtion form={form} suggestionQuery={form.values.name} />

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Implemented in commit 3d1db6c.

The transaction details page now only passes suggestionQuery in create mode, so edit mode no longer triggers auto-apply from AI suggestions and existing avatars are preserved unless explicitly changed.

Screenshot: behavior-only fix (no visual UI change in this update).

</Center>
<BigTextInput
placeholder="Name"
Expand Down
11 changes: 10 additions & 1 deletion src/components/AddParticipantModal/components/PageSetPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UseFormReturnType } from "@mantine/form";
import { Participant } from "@/types";
import BigTextInput from "@/components/BigTextInput";
import PaymentForm from "@/components/PaymentForm";
import { randomPersonEmoji } from "@/utils/randomEmoji";

type PageSetPaymentProps = {
disabledPreferredPaymentMethod?: boolean;
Expand All @@ -18,7 +19,15 @@ const PageSetPayment = ({
<Container>
<Stack gap="xs">
<Center>
<EmojiActionButtion form={form} />
<EmojiActionButtion
form={form}
onRandomize={() =>
form.setFieldValue("avatar", {
emoji: randomPersonEmoji(),
unified: "",
})
}
/>
</Center>
<Center>
<BigTextInput
Expand Down
3 changes: 2 additions & 1 deletion src/components/AddParticipantModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const AddParticipantModal = ({
: null,
"paymentMethod.iban":
!disabledPreferredPaymentMethod &&
values.selectedPaymentMethod === PaymentMethodType.Iban
values.selectedPaymentMethod === PaymentMethodType.Iban &&
values.paymentMethod.iban.trim().length > 0
? validateIban(values.paymentMethod.iban)
Comment thread
guid3d marked this conversation as resolved.
: null,
};
Expand Down
94 changes: 74 additions & 20 deletions src/components/EmojiActionButtion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
Modal as MantineModal,
Stack,
Indicator,
Loader,
} from "@mantine/core";
import { Theme } from "emoji-picker-react";
import dynamic from "next/dynamic";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { IconChevronLeft } from "@tabler/icons-react";
import { IconChevronLeft, IconPencil, IconRefresh } from "@tabler/icons-react";
import { UseFormReturnType } from "@mantine/form";
import { IconPencil } from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react";
import { useEmojiSuggestions } from "@/hooks/useEmojiSuggestions";

const Picker = dynamic(
() => {
Expand All @@ -23,15 +25,38 @@ const Picker = dynamic(

type EmojiActionButtionProps = {
form: UseFormReturnType<any>;
suggestionQuery?: string;
Comment thread
guid3d marked this conversation as resolved.
onRandomize?: () => void;
};

const EmojiActionButtion = ({ form }: EmojiActionButtionProps) => {
const EmojiActionButtion = ({
form,
suggestionQuery,
onRandomize,
}: EmojiActionButtionProps) => {
const [opened, { open, close }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 50em)") || false;
// const [chosenEmoji, setChosenEmoji] = useState<StoreEmojiData>({
// emoji: form.values.avatar.emoji,
// unified: form.values.avatar.unified,
// });
const wasManuallySet = useRef(false);
const [suggestionIndex, setSuggestionIndex] = useState(0);
const { suggestions, loading } = useEmojiSuggestions(suggestionQuery ?? "");

useEffect(() => {
Comment thread
guid3d marked this conversation as resolved.
if (!wasManuallySet.current && suggestions.length > 0) {
form.setFieldValue("avatar", { emoji: suggestions[0], unified: "" });
setSuggestionIndex(0);
}
}, [suggestions]);
Comment thread
guid3d marked this conversation as resolved.

const handleCycle = () => {
if (suggestions.length < 2) return;
const next = (suggestionIndex + 1) % suggestions.length;
setSuggestionIndex(next);
form.setFieldValue("avatar", { emoji: suggestions[next], unified: "" });
};

const showCycleButton =
suggestions.length > 1 && !wasManuallySet.current && !opened;

return (
<>
<MantineModal.Root
Expand Down Expand Up @@ -68,15 +93,14 @@ const EmojiActionButtion = ({ form }: EmojiActionButtionProps) => {
</Center>
<Center>
<Picker
// style={{ border: "none" }}
height={390}
theme={Theme.AUTO}
onEmojiClick={(res) => {
wasManuallySet.current = true;
form.setFieldValue("avatar", {
emoji: res.emoji,
unified: res.unified,
});
// setChosenEmoji({ emoji: res.emoji, unified: res.unified });
close();
}}
previewConfig={{ showPreview: false }}
Expand All @@ -90,23 +114,53 @@ const EmojiActionButtion = ({ form }: EmojiActionButtionProps) => {
{!opened && (
<Indicator
color="gray"
onClick={open}
offset={13}
position="bottom-end"
size={35}
withBorder
label={<IconPencil size={20} width={20} height={20} stroke={1.5} />}
onClick={open}
>
<ActionIcon
variant="default"
size={rem(100)}
radius={rem(100)}
onClick={open}
>
<Title order={1} style={{ fontSize: rem(60) }}>
{form.values.avatar.emoji}
</Title>
</ActionIcon>
<div style={{ position: "relative" }}>
<ActionIcon
variant="default"
size={rem(100)}
radius={rem(100)}
onClick={open}
style={{
opacity: loading && !wasManuallySet.current ? 0.5 : 1,
transition: "opacity 0.2s",
}}
>
{loading && !wasManuallySet.current ? (
<Loader size="sm" />
) : (
<Title order={1} style={{ fontSize: rem(60) }}>
{form.values.avatar.emoji}
</Title>
)}
</ActionIcon>
{(showCycleButton || onRandomize) && (
<ActionIcon
variant="default"
size={rem(28)}
radius={rem(28)}
style={{
position: "absolute",
bottom: 0,
left: 0,
zIndex: 1,
}}
onClick={(e) => {
e.stopPropagation();
if (onRandomize) onRandomize();
else handleCycle();
}}
>
<IconRefresh size={14} stroke={1.5} />
</ActionIcon>
)}
</div>
</Indicator>
)}
</>
Expand Down
28 changes: 15 additions & 13 deletions src/components/ToggleDarkLightMode.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import {
rem,
UnstyledButton,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import { IconMoonFilled, IconSunFilled } from "@tabler/icons-react";
import {
IconBrightnessAutoFilled,
IconMoonFilled,
IconSunFilled,
} from "@tabler/icons-react";

const ToggleDarkLightMode = () => {
const { colorScheme, setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme();
const toggleColorScheme = () => {
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");

const cycleColorScheme = () => {
if (colorScheme === "light") setColorScheme("dark");
else if (colorScheme === "dark") setColorScheme("auto");
else setColorScheme("light");
};

return (
<>
<UnstyledButton lightHidden onClick={() => toggleColorScheme()}>
<IconMoonFilled style={{ width: rem(16) }} />
</UnstyledButton>
<UnstyledButton darkHidden onClick={() => toggleColorScheme()}>
<IconSunFilled style={{ width: rem(16) }} />
</UnstyledButton>
</>
<UnstyledButton onClick={cycleColorScheme}>
{colorScheme === "light" && <IconSunFilled style={{ width: rem(16) }} />}
{colorScheme === "dark" && <IconMoonFilled style={{ width: rem(16) }} />}
{colorScheme === "auto" && <IconBrightnessAutoFilled style={{ width: rem(16) }} />}
</UnstyledButton>
);
};

Expand Down
23 changes: 13 additions & 10 deletions src/components/UserDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
Stack,
Text,
Title,
useComputedColorScheme,
useMantineColorScheme,
} from "@mantine/core";
import UserAvatar from "@/components/UserAvatar";
import {
IconBrightnessAutoFilled,
IconCreditCard,
IconMoonFilled,
IconSunFilled,
Expand All @@ -32,13 +32,14 @@ const UserDropdown = ({
participants,
onSelectUser,
}: UserDropdownProps) => {
const { setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [paymentModalOpened, { open: openPaymentModal, close: closePaymentModal }] = useDisclosure(false);

const toggleColorScheme = () => {
setColorScheme(computedColorScheme === "dark" ? "light" : "dark");
const cycleColorScheme = () => {
if (colorScheme === "light") setColorScheme("dark");
else if (colorScheme === "dark") setColorScheme("auto");
else setColorScheme("light");
};

const handleSelectUser = (participantId: string) => {
Expand Down Expand Up @@ -103,15 +104,17 @@ const UserDropdown = ({

<Menu.Item
leftSection={
computedColorScheme === "dark" ? (
<IconSunFilled style={{ width: rem(14) }} />
) : (
colorScheme === "light" ? (
<IconMoonFilled style={{ width: rem(14) }} />
) : colorScheme === "dark" ? (
<IconBrightnessAutoFilled style={{ width: rem(14) }} />
) : (
<IconSunFilled style={{ width: rem(14) }} />
)
}
onClick={toggleColorScheme}
onClick={cycleColorScheme}
>
{computedColorScheme === "dark" ? "Light mode" : "Dark mode"}
{colorScheme === "light" ? "Dark mode" : colorScheme === "dark" ? "Auto mode" : "Light mode"}
</Menu.Item>
</Menu.Dropdown>
</Menu>
Expand Down
1 change: 1 addition & 0 deletions src/components/UserSelectionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const UserSelectionModal = ({
<Button
onClick={handleSave}
loading={updateParticipant.isPending}
loaderProps={{ type: "dots" }}
fullWidth
>
Save
Expand Down
Loading