Skip to content

Commit

Permalink
Merge pull request #34 from Reynard-G/development
Browse files Browse the repository at this point in the history
Add `Atkinson Hyperlegible` font + Notification Settings
  • Loading branch information
Reynard-G authored Oct 2, 2024
2 parents 197eeb0 + 2e6c5b9 commit 4a0648b
Show file tree
Hide file tree
Showing 21 changed files with 407 additions and 58 deletions.
7 changes: 3 additions & 4 deletions app/components/ChangePasswordButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import {
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { AccountSettingsFetcherResponse } from "~/routes/app.settings.account";

import { SpokeSpinner } from "./ui/spinner";
import { SpokeSpinner } from "~/components/ui/spinner";
import { action } from "~/routes/app.$server.transactions";

interface ChangePasswordButtonProps {
label: string;
Expand All @@ -33,7 +32,7 @@ export default function ChangePasswordButton({
variant = "default",
children,
}: ChangePasswordButtonProps) {
const fetcher = useFetcher<AccountSettingsFetcherResponse>();
const fetcher = useFetcher<typeof action>();
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const [oldPassword, setOldPassword] = useState<string>("");
const [newPassword, setNewPassword] = useState<string>("");
Expand Down
9 changes: 2 additions & 7 deletions app/components/Dialog/CreateTransactionsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,12 @@ import { Skeleton } from "~/components/ui/skeleton";
import { SpokeSpinner } from "~/components/ui/spinner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useMediaQuery } from "~/hooks/use-media-query";
import { type loader } from "~/routes/app.$server.transactions";
import { action, type loader } from "~/routes/app.$server.transactions";
import { NonSensitiveUser } from "~/types/User";

export type CreateTransactionsDialogFetcherResponse = {
success: boolean;
message?: string;
};

export default function CreateTransactionsDialog() {
const { allUsers } = useLoaderData<typeof loader>();
const fetcher = useFetcher<CreateTransactionsDialogFetcherResponse>();
const fetcher = useFetcher<typeof action>();
const isDesktop = useMediaQuery("(min-width: 768px)");
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const [isUsersPopoverOpen, setIsUsersPopoverOpen] = useState<boolean>(false);
Expand Down
9 changes: 2 additions & 7 deletions app/components/Dialog/ExportTransactionsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,10 @@ import {
type ExportOptions,
exportTransactionsTable,
} from "~/lib/utils/exportTable";

export type ExportTransactionsDialogFetcherResponse = {
success: boolean;
message?: string;
data?: Transaction[];
};
import { ExportActionData } from "~/routes/app.$server.transactions";

export default function ExportTransactionsDialog() {
const fetcher = useFetcher<ExportTransactionsDialogFetcherResponse>();
const fetcher = useFetcher<ExportActionData>();
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
const [fileType, setFileType] = useState<ExportOptions["format"]>("csv");
const [filename, setFilename] = useState<string>(
Expand Down
30 changes: 30 additions & 0 deletions app/components/Switch/SettingsSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Switch } from "~/components/ui/switch";

interface SettingsSwitchProps {
name: string;
label: string;
defaultChecked?: boolean;
description?: string;
disabled?: boolean;
}

export default function SettingsSwitch({
name,
label,
defaultChecked = false,
description,
disabled = false,
}: SettingsSwitchProps) {
return (
<div className="flex flex-row items-center justify-between space-y-2 rounded-lg border p-4">
<div className="space-y-0.5">
<label className="text-base font-semibold peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{label}
</label>
<p className="text-sm text-muted-foreground">{description}</p>
</div>

<Switch name={name} defaultChecked={defaultChecked} disabled={disabled} />
</div>
);
}
27 changes: 27 additions & 0 deletions app/components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";

import { cn } from "~/lib/utils/cn";

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;

export { Switch };
6 changes: 6 additions & 0 deletions app/lib/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { sql } from "drizzle-orm";
import {
boolean,
index,
integer,
numeric,
Expand Down Expand Up @@ -136,6 +137,11 @@ export const userSettings = pgTable(identifyTable("UserSettings"), {
onUpdate: "cascade",
}),
font: text("font").notNull().default("Default"),
discordCommunication: boolean("discord_communication")
.notNull()
.default(false),
discordTransactions: boolean("discord_transactions").notNull().default(true),
discordSecurity: boolean("discord_security").notNull().default(true),
});
export type UserSetting = typeof userSettings.$inferSelect;
export type NewUserSetting = typeof userSettings.$inferInsert;
Expand Down
22 changes: 22 additions & 0 deletions app/lib/get.queries.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ export async function getAppearanceSettings(
};
}

/**
* Get notification settings for a user.
*
* @param userId The ID of the user.
* @returns The notification settings.
*/
export async function getNotificationSettings(
userId: number,
): Promise<Partial<UserSetting>> {
const userSettings = await getUserSettingsById(userId);

if (!userSettings) {
throw new Error("User settings not found");
}

return {
discordCommunication: userSettings.discordCommunication,
discordTransactions: userSettings.discordTransactions,
discordSecurity: userSettings.discordSecurity,
};
}

/**
* Get transactions based on the input provided. This function is used to
* fetch transactions for the transactions table using the search parameters
Expand Down
25 changes: 24 additions & 1 deletion app/lib/post.queries.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,30 @@ export async function updateAccountSettings(
*/
export async function updateAppearanceSettings(
userId: number,
settings: Omit<Partial<UserSetting>, "id" | "user_id">,
settings: Omit<
Partial<UserSetting>,
| "id"
| "user_id"
| "discordCommunication"
| "discordTransactions"
| "discordSecurity"
>,
): Promise<void> {
await db
.update(userSettings)
.set(settings)
.where(eq(userSettings.userId, userId));
}

/**
* Get a user's notification settings.
*
* @param userId The ID of the user.
* @returns The user's notification settings.
*/
export async function updateNotificationSettings(
userId: number,
settings: Omit<Partial<UserSetting>, "id" | "user_id" | "font">,
): Promise<void> {
await db
.update(userSettings)
Expand Down
60 changes: 39 additions & 21 deletions app/routes/app.$server.transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import {
type ActionFunctionArgs,
json,
type LoaderFunctionArgs,
type TypedResponse,
unstable_parseMultipartFormData,
} from "@remix-run/node";
import { Await, defer, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
import { namedAction } from "remix-utils/named-action";

import { TransactionsTable } from "~/components/DataTable/TransactionsTable";
import { CreateTransactionsDialogFetcherResponse } from "~/components/Dialog/CreateTransactionsDialog";
import { ExportTransactionsDialogFetcherResponse } from "~/components/Dialog/ExportTransactionsDialog";
import { Separator } from "~/components/ui/separator";
import { SpokeSpinner } from "~/components/ui/spinner";
import { Transaction } from "~/lib/db/schema";
import {
getAllTransactions,
getNonSensitiveUserInfo,
Expand All @@ -26,6 +26,23 @@ import { uploadHandler } from "~/lib/services/s3.server";
import { getErrorMessage } from "~/lib/utils/getErrorMessage";
import { searchParamsSchema } from "~/lib/validations";

export type DepositActionData = { success: boolean; message: string };
export type WithdrawActionData = { success: boolean; message: string };
export type TransferActionData = { success: boolean; message: string };
export type ExportActionData = {
success: boolean;
message: string;
data?: Transaction[];
};

type ActionData =
| DepositActionData
| WithdrawActionData
| TransferActionData
| ExportActionData;

export type ActionReturn = TypedResponse<ActionData>;

export async function loader({ request, params }: LoaderFunctionArgs) {
const url = new URL(request.url);
const search = searchParamsSchema.parse(Object.fromEntries(url.searchParams));
Expand Down Expand Up @@ -77,12 +94,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
const server = params.server;

return namedAction(request, {
async deposit() {
async deposit(): Promise<ActionReturn> {
const amount = formData.get("amount")?.toString();
const proofOfDeposit = formData.get("proofOfDeposit")?.toString();

if (!amount || !proofOfDeposit) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<DepositActionData>(
{
success: false,
message: "Amount and proof of deposit are required",
Expand All @@ -92,21 +109,21 @@ export async function action({ request, params }: ActionFunctionArgs) {
}

if (typeof amount === "string" && isNaN(Number(amount))) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<DepositActionData>(
{ success: false, message: "Invalid amount" },
{ status: 400 },
);
}

return deposit(userId, Number(amount), proofOfDeposit, server)
.then(() =>
json<CreateTransactionsDialogFetcherResponse>({
json<DepositActionData>({
success: true,
message: "Deposit successful",
}),
)
.catch((error) =>
json<CreateTransactionsDialogFetcherResponse>(
json<DepositActionData>(
{
success: false,
message: "Deposit failed: " + getErrorMessage(error),
Expand All @@ -115,32 +132,32 @@ export async function action({ request, params }: ActionFunctionArgs) {
),
);
},
async withdraw() {
async withdraw(): Promise<ActionReturn> {
const amount = formData.get("amount")?.toString();

if (!amount) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<WithdrawActionData>(
{ success: false, message: "Amount is required" },
{ status: 400 },
);
}

if (typeof amount === "string" && isNaN(Number(amount))) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<WithdrawActionData>(
{ success: false, message: "Invalid amount" },
{ status: 400 },
);
}

return withdraw(userId, Number(amount), server)
.then(() =>
json<CreateTransactionsDialogFetcherResponse>({
json<WithdrawActionData>({
success: true,
message: "Withdrawal successful",
}),
)
.catch((error) =>
json<CreateTransactionsDialogFetcherResponse>(
json<WithdrawActionData>(
{
success: false,
message: "Withdrawal failed: " + getErrorMessage(error),
Expand All @@ -149,12 +166,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
),
);
},
async transfer() {
async transfer(): Promise<ActionReturn> {
const amount = formData.get("amount")?.toString();
const recipient = formData.get("recipient")?.toString();

if (!amount || !recipient) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<TransferActionData>(
{
success: false,
message: "Amount and recipient are required",
Expand All @@ -164,28 +181,28 @@ export async function action({ request, params }: ActionFunctionArgs) {
}

if (typeof amount === "string" && isNaN(Number(amount))) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<TransferActionData>(
{ success: false, message: "Invalid amount" },
{ status: 400 },
);
}

if (recipient.toString() === userId.toString()) {
return json<CreateTransactionsDialogFetcherResponse>(
return json<TransferActionData>(
{ success: false, message: "Unable to transfer to yourself" },
{ status: 400 },
);
}

return transfer(userId, Number(recipient), Number(amount), server)
.then(() =>
json<CreateTransactionsDialogFetcherResponse>({
json<TransferActionData>({
success: true,
message: "Transfer successful",
}),
)
.catch((error) =>
json<CreateTransactionsDialogFetcherResponse>(
json<TransferActionData>(
{
success: false,
message: "Transfer failed: " + getErrorMessage(error),
Expand All @@ -194,16 +211,17 @@ export async function action({ request, params }: ActionFunctionArgs) {
),
);
},
async export() {
async export(): Promise<ActionReturn> {
return getAllTransactions(userId, server)
.then((transactions) =>
json<ExportTransactionsDialogFetcherResponse>({
json<ExportActionData>({
success: true,
message: "Exported transactions successfully",
data: transactions,
}),
)
.catch((error) =>
json<ExportTransactionsDialogFetcherResponse>(
json<ExportActionData>(
{
success: false,
message: "Export failed: " + getErrorMessage(error),
Expand Down
Loading

0 comments on commit 4a0648b

Please sign in to comment.