Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reviewing Open Saas code #369

Draft
wants to merge 2 commits into
base: filip-empty
Choose a base branch
from
Draft

Reviewing Open Saas code #369

wants to merge 2 commits into from

Conversation

sodic
Copy link
Collaborator

@sodic sodic commented Feb 17, 2025

No description provided.

- Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project.
- Run the database with `wasp start db` and leave it running.
- Run `wasp start` and leave it running.
- [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Optional is probably not the right word here, since it's mandatory when it's mandatory, and has no effect otherwise.

import { useState } from 'react';
import { cn } from '../../../client/cn';

const SwitcherOne = ({ user, updateIsUserAdminById }: { user?: Partial<User>; updateIsUserAdminById?: any }) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why are we passing updateIsUserAdminById through a prop instead of importing it directly?

Comment on lines +14 to +20
if (!context.user) {
throw new HttpError(401);
}

if (!context.user.isAdmin) {
throw new HttpError(403);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Until we get a proper middleware, we should probably cook up our own decorators for this (or at least call a function that checks everything and throws.

Comment on lines +22 to +31
const updatedUser = await context.entities.User.update({
where: {
id,
},
data: {
isAdmin: data.isAdmin,
},
});

return updatedUser;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

updatedUser can be returned directly, meaning that the await is redundant as well.

return updatedUser;
};

export const updateCurrentUserLastActiveTimestamp: UpdateCurrentUserLastActiveTimestamp<Pick<User, 'lastActiveTimestamp'>, User> = async ({ lastActiveTimestamp }, context) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We have plenty of long lines in this file (and other files). How come prettier doesn't take care of it? We should look into that

const trigger = useRef<any>(null);
const dropdown = useRef<any>(null);

const toggleDropdown = () => setDropdownOpen((prev) => !prev);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I might be wrong, but I have a feeling React has a more native support for this (more precisely, I don't think we need this wrapper).

Comment on lines +10 to +11
const trigger = useRef<any>(null);
const dropdown = useRef<any>(null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is strange, both because of useRef (which should be used with cautionandany`.

<span className='block text-sm font-medium dark:text-white'>{user.username}</span>
</span>
<CgProfile size='1.1rem' className='ml-1 mt-[0.1rem] dark:text-white' />
<svg
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Again , we should probably extract SVGs into their own files.

);
};

export default DropdownUser;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I advise against using defualt exports.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: Continue tomorrow (stopped at DropdownUser)

Copy link
Collaborator Author

@sodic sodic left a comment

Choose a reason for hiding this comment

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

Second pass, superficial review of most modules.

Comment on lines +3 to +4
export const paymentsWebhook = paymentProcessor.webhook;
export const paymentsMiddlewareConfigFn = paymentProcessor.webhookMiddlewareConfigFn;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This pattern looks weird, I should investigate what's going on here.

@@ -0,0 +1,8 @@
export function requireNodeEnvVar(name: string): string {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think something like this is now supported through Wasp via Zod.

Comment on lines +24 to +31
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const subscriptionStatus = faker.helpers.arrayElement<SubscriptionStatus | null>(['active', 'cancel_at_period_end', 'past_due', 'deleted', null]);
const now = new Date();
const createdAt = faker.date.past({ refDate: now });
const lastActiveTimestamp = faker.date.between({ from: createdAt, to: now });
const credits = subscriptionStatus ? 0 : faker.number.int({ min: 0, max: 10 });
const hasUserPaidOnStripe = !!subscriptionStatus || credits > 3
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seems like some of these are redundant.

Comment on lines +1 to +2
export const DocsUrl = 'https://docs.opensaas.sh';
export const BlogUrl = 'https://docs.opensaas.sh/blog';
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These should be in all caps or in camelCase.

* For more info see: https://wasp.sh/docs/data-model/backends#seeding-the-database
*/
export async function seedMockUsers(prismaClient: PrismaClient) {
await Promise.all(generateMockUsersData(50).map((data) =>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The await is redundant.

Comment on lines +306 to +314
const priorityOrder = ['low', 'medium', 'high'];
if (a.props.mainTask.priority && b.props.mainTask.priority) {
return (
priorityOrder.indexOf(b.props.mainTask.priority) - priorityOrder.indexOf(a.props.mainTask.priority)
);
} else {
return 0;
}
})
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This should probably be its own function.

)}
</table>

{/* ))} */}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What's this for?

Comment on lines +23 to +29
const shouldDisplayAppNavBar = useMemo(() => {
return location.pathname !== routes.LoginRoute.build() && location.pathname !== routes.SignupRoute.build();
}, [location]);

const isAdminDashboard = useMemo(() => {
return location.pathname.startsWith('/admin');
}, [location]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why useMemo, was there a performance hit?

return location.pathname.startsWith('/admin');
}, [location]);

useEffect(() => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should name the useEffect.

const lastSeenAt = new Date(user.lastActiveTimestamp);
const today = new Date();
if (today.getTime() - lastSeenAt.getTime() > 5 * 60 * 1000) {
updateCurrentUserLastActiveTimestamp({ lastActiveTimestamp: today });
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As mentioned, the timestamp sould be calculated on the backend.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we need this


const Header = (props: {
sidebarOpen: string | boolean | undefined;
setSidebarOpen: (arg0: boolean) => void;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should name the argument.

Comment on lines +24 to +25
// close on click outside
useEffect(() => {
Copy link
Collaborator Author

@sodic sodic Feb 19, 2025

Choose a reason for hiding this comment

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

We should name the useEffect.

Applies to other places too.

Comment on lines +16 to +17
const trigger = useRef<any>(null);
const sidebar = useRef<any>(null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should remove the anys.

const trigger = useRef<any>(null);
const sidebar = useRef<any>(null);

const storedSidebarExpanded = localStorage.getItem('sidebar-expanded');
Copy link
Collaborator Author

@sodic sodic Feb 19, 2025

Choose a reason for hiding this comment

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

This probably isn't the best way to do this in React.

Applies to other places too.

}
}, [sidebarExpanded]);

return (
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The component body is too long, we should break it up.

import { useState } from 'react';
import { cn } from '../../../client/cn';

const CheckboxOne = () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't know how checkbox are done these days, but this seems like a lot.

const FormElements = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });

return (
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Long component, we should break it up.

import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';

const FormElements = ({ user }: { user: AuthUser }) => {
useRedirectHomeUnlessUserIsAdmin({ user });
Copy link
Collaborator Author

@sodic sodic Feb 19, 2025

Choose a reason for hiding this comment

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

Perhaps it makes sense extracting this into a decorator.

Maybe it's even something Wasp should add first -lass support for.

import { useState } from 'react';
import { cn } from '../../../client/cn';

const SwitcherTwo = () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can we make these components names more informative (e.g., SwitcherOne, CheckboxOne). I don't know what they do. Perhaps they're different designs of the same functionality. If that's the case, we should maybe somehow extract this functionality.

Comment on lines +37 to +38
let userDelta = userCount;
let paidUserDelta = paidUserCount;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Doesn't seem like these guys should be lets.


let userDelta = userCount;
let paidUserDelta = paidUserCount;
if (yesterdaysStats) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The condition could be more explicit.

totalRevenue = await fetchTotalLemonSqueezyRevenue();
break;
default:
throw new Error(`Unsupported payment processor: ${paymentProcessor.id}`);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should add an exhaustiveness check.

throw new Error(`Unsupported payment processor: ${paymentProcessor.id}`);
}

const { totalViews, prevDayViewsChangePercent } = await getDailyPageViews();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function is getting pretty long, we should break it up.

await context.entities.Logs.create({
data: {
message: `Error calculating daily stats: ${error?.message}`,
level: 'job-error',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Double-check if these levels are enumerated somewhere.

};

let hasMore = true;
while (hasMore) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function should probably be refactored.

let hasNextPage = true;
let currentPage = 1;

while (hasNextPage) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another while here (and a lot of lets), chances are we could improve it.

// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
import { paymentProcessor } from '../payment/paymentProcessor';

export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?: DailyStats[]; isLoading?: boolean };
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could improve this type, it currently allows invalid states.

},
});
if (!dailyStats) {
console.log('\x1b[34mNote: No daily stats have been generated by the dailyStatsJob yet. \x1b[0m');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Inline ANSI symbols look weird without a name/function describing them.


async function getTotalPageViews() {
const response = await fetch(
`${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&metrics=pageviews`,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Constructing URLs like this is potentially dangerous. We should use new URL.

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = (await response.json()) as PageViewsResult;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unnecessary assertion.

Comment on lines +5 to +8
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${PLAUSIBLE_API_KEY}`,
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is repeated in several places, we should extract and specify a type for it.


export async function getSources() {
const [response] = await analyticsDataClient.runReport({
property: `properties/${PROPERTY_ID}`,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Possible problems here, we should validate this env var with Zod.

],
});

let activeUsersPerReferrer: any[] = [];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another any.

});
let totalViews = 0;
if (response?.rows) {
// @ts-ignore
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see many ts-ignores in this file.

Comment on lines +118 to +119
let viewsFromYesterday;
let viewsFromDayBeforeYesterday;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see many lets in this file.

root: 'body',
autoShow: true,
disablePageInteraction: false,
hideFromBots: import.meta.env.PROD ? true : false, // Set this to false for dev/headless tests otherwise the modal will not be visible.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seems like a redundant ternary operator. But we should validate the env var with Zod.

services: {
ga: {
label: 'Google Analytics',
onAccept: () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is this code provided by Google or did we write it ourselves?

<input
type='checkbox'
onChange={() => {
if (typeof setColorMode === 'function') {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why is this check necessary?

import { useEffect, useRef, useState } from 'react';
import { cn } from '../../../client/cn';

const DropdownDefault = () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The code is this component looks very familiar, I think it's duplicated somewhere else.

Comment on lines +7 to +8
const trigger = useRef<any>(null);
const dropdown = useRef<any>(null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We shouldn't be using any.

Comment on lines +9 to +10
const [skip, setskip] = useState(0);
const [page, setPage] = useState(1);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The pagination should most likely be its own hook.

Comment on lines +11 to +12
const [email, setEmail] = useState<string | undefined>(undefined);
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Better to use null than undefined.

Comment on lines +93 to +104
onChange={(e) => {
const targetValue = e.target.value === '' ? null : e.target.value;
setStatusOptions((prevValue) => {
if (prevValue?.includes(targetValue as SubscriptionStatus)) {
return prevValue?.filter((val) => val !== targetValue);
} else if (!!prevValue) {
return [...prevValue, targetValue as SubscriptionStatus];
} else {
return prevValue;
}
});
}}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a lot of code for an annonymous function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This code should be under the same mode as the server-side code used to calculate the analytics.

Comment on lines +122 to +131
const daysOfWeekArr = useMemo(() => {
if (!!weeklyStats && weeklyStats?.length > 0) {
const datesArr = weeklyStats?.map((stat) => {
// get day of week, month, and day of month
const dateArr = stat.date.toString().split(' ');
return dateArr.slice(0, 3).join(' ');
});
return datesArr;
}
}, [weeklyStats]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Many magic numbers and complex conditions in this function. We should extract variables.

Comment on lines +178 to +193
if (!!daysOfWeekArr && daysOfWeekArr?.length > 0 && !!dailyRevenueArray && dailyRevenueArray?.length > 0) {
setChartOptions({
...options,
xaxis: {
...options.xaxis,
categories: daysOfWeekArr,
},
yaxis: {
...options.yaxis,
// get the min & max values to the neareast hundred
max: Math.ceil(Math.max(...dailyRevenueArray) / 100) * 100,
min: Math.floor(Math.min(...dailyRevenueArray) / 100) * 100,
},
});
}
}, [daysOfWeekArr, dailyRevenueArray]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is very complex for an annonymous function.

}[];
}

const RevenueAndProfitChart = ({ weeklyStats, isLoading }: DailyStatsProps) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This component is pretty long, we should break it up.

@@ -0,0 +1,47 @@
import { type PageViewSource } from 'wasp/entities';

const SourcesTable = ({ sources }: { sources: PageViewSource[] | undefined }) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why leave the option of passing in undefined?

Comment on lines +4 to +7
type PageViewsStats = {
totalPageViews: number | undefined;
prevDayViewsChangePercent: string | undefined;
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This type is imprecise, it allows invalid states.

Comment on lines +7 to +9
const isDeltaPositive = useMemo(() => {
return !!dailyStats?.paidUserDelta && dailyStats?.paidUserDelta > 0;
}, [dailyStats]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this useMemo is necessary.

Comment on lines +6 to +9
const isDeltaPositive = useMemo(() => {
if (!weeklyStats) return false;
return (weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) > 0;
}, [weeklyStats]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There's some repetitino here, we shuold look into that.

Comment on lines +11 to +19
const deltaPercentage = useMemo(() => {
if ( !weeklyStats || weeklyStats.length < 2 || isLoading) return;
if ( weeklyStats[1]?.totalRevenue === 0 || weeklyStats[0]?.totalRevenue === 0 ) return 0;

weeklyStats.sort((a, b) => b.id - a.id);

const percentage = ((weeklyStats[0].totalRevenue - weeklyStats[1]?.totalRevenue) / weeklyStats[1]?.totalRevenue) * 100;
return Math.floor(percentage);
}, [weeklyStats]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A lot of code for an annonymous function.

Comment on lines +7 to +9
const isDeltaPositive = useMemo(() => {
return !!dailyStats?.userDelta && dailyStats.userDelta > 0;
}, [dailyStats]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

More repetition here, we should look into that.

<VerifyEmailForm />
<br />
<span className='text-sm font-medium text-gray-900'>
If everything is okay, <WaspRouterLink to={routes.LoginRoute.to} className='underline'>go to login</WaspRouterLink>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can we somehow detect whether everything's ok?

Could we log the user in automatically without telling them to do it themselves?

<ResetPasswordForm />
<br />
<span className='text-sm font-medium text-gray-900'>
If everything is okay, <WaspRouterLink to={routes.LoginRoute.to}>go to login</WaspRouterLink>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same questions I had for the EmailVerificationPage.

Comment on lines +7 to +9
username: (data: any) => data.email,
isAdmin: (data: any) => adminEmails.includes(data.email),
email: (data: any) => data.email,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A lot of anys here.

ContentType: `${fileType}`,
};
const command = new PutObjectCommand(s3Params);
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600,});
Copy link
Collaborator Author

@sodic sodic Feb 19, 2025

Choose a reason for hiding this comment

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

What time is 3600 here? We should document it.

I'm guessing it's an hour. Why so long?

Key: key,
};
const command = new GetObjectCommand(s3Params);
return await getSignedUrl(s3Client, command, { expiresIn: 3600 });
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unnecessary await.

return { uploadUrl, key: Key };
}

export const getDownloadFileSignedURLFromS3 = async ({ key }: { key: string }) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Missing return type.

userInfo: string;
}

export const getUploadFileSignedURLFromS3 = async ({fileType, userInfo}: S3Upload) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Missing return type (I think this applies to a lot of code).

Comment on lines +23 to +45
useEffect(() => {
allUserFiles.refetch();
}, []);

useEffect(() => {
if (fileKeyForS3.length > 0) {
refetchDownloadUrl()
.then((urlQuery) => {
switch (urlQuery.status) {
case 'error':
console.error('Error fetching download URL', urlQuery.error);
alert('Error fetching download');
return;
case 'success':
window.open(urlQuery.data, '_blank');
return;
}
})
.finally(() => {
setFileKeyForS3('');
});
}
}, [fileKeyForS3]);
Copy link
Collaborator Author

@sodic sodic Feb 19, 2025

Choose a reason for hiding this comment

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

Unnamed useEffects, missing exhaustiveness check, possibility for early returns, we should kick out then and finally in favor of async/await.

}

await uploadFileWithProgress({ file, setUploadProgressPercent });
formElement.reset();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Weird stuff happening with the form here, we should investigate.

Comment on lines +136 to +159
<h2 className='text-xl font-bold'>Uploaded Files</h2>
{allUserFiles.isLoading && <p>Loading...</p>}
{allUserFiles.error && <p>Error: {allUserFiles.error.message}</p>}
{!!allUserFiles.data && allUserFiles.data.length > 0 && !allUserFiles.isLoading ? (
allUserFiles.data.map((file: File) => (
<div
key={file.key}
className={cn(
'flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3',
{
'opacity-70': file.key === fileKeyForS3 && isDownloadUrlLoading,
}
)}
>
<p>{file.name}</p>
<button
onClick={() => setFileKeyForS3(file.key)}
disabled={file.key === fileKeyForS3 && isDownloadUrlLoading}
className='min-w-[7rem] text-sm text-gray-800/90 bg-purple-50 shadow-md ring-1 ring-inset ring-slate-200 py-1 px-2 rounded-md hover:bg-purple-100 duration-200 ease-in-out focus:outline-none focus:shadow-none hover:shadow-none disabled:cursor-not-allowed'
>
{file.key === fileKeyForS3 && isDownloadUrlLoading ? 'Loading...' : 'Download'}
</button>
</div>
))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The component is very complex, we should break it up.

'video/mp4',
];

export async function uploadFileWithProgress({ file, setUploadProgressPercent }: FileUploadProgress) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Could this be a hook that handles everything?


const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo });

return await context.entities.File.create({
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should we validate the payload?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant