Skip to content

Commit

Permalink
Add Collections Backend (#243)
Browse files Browse the repository at this point in the history
* Add Display Collections PoC

* Add createCollection Function

* Add Collections to Cache

* Dialog PoC

* Add More Collection Dialogs

* Fix Build

* Add Delete Collection Button

* Update PoC

* Update PoC

* Update Next.js Redirect Handling

* Update Create Server Action Handling

Add server action clients that can have different middleware.

* Fix Error Handling in Client Hook

* Update Types

* Add Util Functions

* Remove Public References

* Update Dependencies

* Use Planetscale Serverless
  • Loading branch information
timoclsn authored Oct 7, 2023
1 parent 940f864 commit de11c0d
Show file tree
Hide file tree
Showing 68 changed files with 2,636 additions and 979 deletions.
16 changes: 11 additions & 5 deletions apps/resources/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { About } from './About/About';
import { Header } from './Header/Header';
import { Page } from 'components/Page/Page';
import { SearchParams } from 'lib/types';
import { NewResources } from '../../components/NewResources/NewResources';
import { Newsletter } from '../../components/Newsletter/Newsletter';
import { About } from './About/About';
import { Header } from './Header/Header';

interface Props {
searchParams: SearchParams;
}

const Home = () => {
const Home = ({ searchParams }: Props) => {
return (
<>
<Page searchParams={searchParams}>
<Header />
<NewResources />
<Newsletter />
<About />
</>
</Page>
);
};

Expand Down
76 changes: 76 additions & 0 deletions apps/resources/app/collections/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { auth } from '@clerk/nextjs';
import { Resources } from 'app/resources/Resources/Resources';
import { Await } from 'components/Await/Await';
import { Page } from 'components/Page/Page';
import { Heading, Text } from 'design-system';
import { getCollectionCached } from 'lib/cache';
import { SearchParams } from 'lib/types';
import { DeleteCollectionButton } from '../../../components/Collections/DeleteCollectionButton/DeleteCollectionButton';
import { UpdateBollectionButton } from '../../../components/Collections/UpdateBollectionButton';

interface Props {
params: {
id: string;
};
searchParams: SearchParams;
}

const CollectionPage = async ({ params, searchParams }: Props) => {
const { id } = params;
const promise = getCollectionCached(Number(id));
const { userId } = auth();

return (
<Page searchParams={searchParams}>
<div>
<Heading>Collection Page</Heading>
{userId && (
<div className="flex flex-col items-start gap-4">
<UpdateBollectionButton collectionId={Number(id)} />
<DeleteCollectionButton collectionId={Number(id)} />
</div>
)}
</div>
<Await promise={promise}>
{(collection) => {
if (!collection) return <div>No collection found</div>;
const isOwnCollection = collection.user?.id === userId;

return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Heading>Title: {collection.title}</Heading>
<Text>Description: {collection.description}</Text>
<Text>Username: {collection.user?.username ?? 'anonymos'}</Text>
</div>
{collection.resources.length === 0 ? (
<div>No resources in collection</div>
) : (
<div>
Resources:
<ul className="flex flex-col gap-2">
{collection.resources.map((resource) => {
return (
<>
<li key={`${resource.id}-${resource.type}`}>
{'title' in resource
? resource.title
: resource.name}
</li>
<div>----------------------------------</div>
</>
);
})}
</ul>
</div>
)}
{isOwnCollection && <Resources searchParams={searchParams} />}
</div>
);
}}
</Await>
</Page>
);
};

export default CollectionPage;
46 changes: 46 additions & 0 deletions apps/resources/app/collections/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { auth } from '@clerk/nextjs';
import { Await } from 'components/Await/Await';
import { Page } from 'components/Page/Page';
import { Heading, Text } from 'design-system';
import { getCollectionsCached } from 'lib/cache';
import { SearchParams } from 'lib/types';
import Link from 'next/link';
import { AddCollectionButton } from '../../components/Collections/AddCollectionButton';

interface Props {
searchParams: SearchParams;
}

const CollectionsPage = async ({ searchParams }: Props) => {
const promise = getCollectionsCached();
const { userId } = auth();
return (
<Page searchParams={searchParams}>
<Heading level="2">Collections</Heading>
{userId && <AddCollectionButton />}
<Await promise={promise}>
{(collections) => {
return (
<ul>
{collections.map(({ id, title, description, user }) => (
<li key={id}>
<Link
href={`/collections/${id}`}
className="flex flex-col gap-2"
>
<Heading level="3">Title: {title}</Heading>
<Text>Description: {description}</Text>
<Text>Username: {user?.username ?? 'anonymos'}</Text>
</Link>
<div>----------------------------------</div>
</li>
))}
</ul>
);
}}
</Await>
</Page>
);
};

export default CollectionsPage;
29 changes: 19 additions & 10 deletions apps/resources/app/imprint/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { Page } from 'components/Page/Page';
import { allPages } from 'contentlayer/generated';
import { Heading } from 'design-system';
import { SearchParams } from 'lib/types';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Imprint',
};

const ImprintPage = () => {
interface Props {
searchParams: SearchParams;
}

const ImprintPage = ({ searchParams }: Props) => {
const content = allPages.find((page) => page.title === 'Imprint');
return (
<section className="mx-auto max-w-prose space-y-20">
<Heading level="1" className="mb-6">
{content?.title}
</Heading>
<div
className="prose"
dangerouslySetInnerHTML={{ __html: content?.body.html ?? '' }}
/>
</section>
<Page searchParams={searchParams}>
<section className="mx-auto max-w-prose space-y-20">
<Heading level="1" className="mb-6">
{content?.title}
</Heading>
<div
className="prose"
dangerouslySetInnerHTML={{ __html: content?.body.html ?? '' }}
/>
</section>
</Page>
);
};

export default ImprintPage;
28 changes: 18 additions & 10 deletions apps/resources/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { Page } from 'components/Page/Page';
import { allPages } from 'contentlayer/generated';
import { Heading } from 'design-system';
import { SearchParams } from 'lib/types';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Privacy Policy',
};

const PrivacyPage = () => {
interface Props {
searchParams: SearchParams;
}

const PrivacyPage = ({ searchParams }: Props) => {
const content = allPages.find((page) => page.title === 'Privacy');
return (
<section className="mx-auto max-w-prose space-y-20">
<Heading level="1" className="mb-6">
{content?.title}
</Heading>
<div
className="prose"
dangerouslySetInnerHTML={{ __html: content?.body.html ?? '' }}
/>
</section>
<Page searchParams={searchParams}>
<section className="mx-auto max-w-prose space-y-20">
<Heading level="1" className="mb-6">
{content?.title}
</Heading>
<div
className="prose"
dangerouslySetInnerHTML={{ __html: content?.body.html ?? '' }}
/>
</section>
</Page>
);
};

Expand Down
2 changes: 1 addition & 1 deletion apps/resources/app/profile/DeleteAccountButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useAuth } from '@clerk/nextjs';
import { Alert, Button, InfoBox } from 'design-system';
import { AlertTriangle, CheckCircle2, Loader2, XCircle } from 'lucide-react';
import { useAction } from '../../lib/actions/useAction';
import { useAction } from '../../lib/serverActions/client';
import { deleteAccount } from './actions';

export const DeleteAccountButton = () => {
Expand Down
8 changes: 2 additions & 6 deletions apps/resources/app/profile/actions.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
'use server';

import { clerkClient } from '@clerk/nextjs';
import { createProtectedAction } from 'lib/serverActions/create';
import { revalidatePath } from 'next/cache';
import { createAction } from '../../lib/actions/createAction';
import { deleteUserData } from '../../lib/resources';

export const deleteAccount = createAction({
export const deleteAccount = createProtectedAction({
action: async ({ ctx }) => {
const { userId } = ctx;

if (!userId) {
throw new Error('You must be logged in to delete your account.');
}

try {
await clerkClient.users.deleteUser(userId);
await deleteUserData(userId);
Expand Down
58 changes: 33 additions & 25 deletions apps/resources/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,45 @@ import {
SignedOut,
UserProfile,
} from '@clerk/nextjs';
import { Page } from 'components/Page/Page';
import { Heading } from 'design-system';
import { SearchParams } from 'lib/types';
import { DeleteAccountButton } from './DeleteAccountButton';

const ProfilePage = () => {
interface Props {
searchParams: SearchParams;
}

const ProfilePage = ({ searchParams }: Props) => {
return (
<section className="mx-auto max-w-4xl">
<Heading level="1" className="mb-8">
Profile
</Heading>
<SignedIn>
<div className="flex flex-col items-center justify-center gap-10">
<UserProfile
appearance={{
variables: {
colorPrimary: '#101b2c',
borderRadius: 'none',
},
elements: {
card: 'shadow-none',
},
}}
/>
<Page searchParams={searchParams}>
<section className="mx-auto max-w-4xl">
<Heading level="1" className="mb-8">
Profile
</Heading>
<SignedIn>
<div className="flex flex-col items-center justify-center gap-10">
<DeleteAccountButton />
<UserProfile
appearance={{
variables: {
colorPrimary: '#101b2c',
borderRadius: 'none',
},
elements: {
card: 'shadow-none',
},
}}
/>
<div className="flex flex-col items-center justify-center gap-10">
<DeleteAccountButton />
</div>
</div>
</div>
</SignedIn>
<SignedOut>
<RedirectToSignIn />
</SignedOut>
</section>
</SignedIn>
<SignedOut>
<RedirectToSignIn />
</SignedOut>
</section>
</Page>
);
};

Expand Down
24 changes: 21 additions & 3 deletions apps/resources/app/resources/Resources/Resources.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { Heading, Text } from 'design-system';
import { SearchParams } from 'lib/types';
import { z } from 'zod';
import { formateDate } from '../../../lib/utils';
import { ReseourcesFilter } from '../page';
import { ResourcesTable } from './ResourcesTable/ResourcesTable';

export type ReseourcesFilter = z.infer<typeof reseourcesFilterSchema>;
const reseourcesFilterSchema = z.object({
type: z.coerce.string().optional(),
category: z.coerce.string().optional(),
topic: z.coerce.string().optional(),
sort: z.coerce.string().optional(),
search: z.coerce.string().optional(),
likes: z.coerce.boolean().optional(),
comments: z.coerce.boolean().optional(),
from: z.coerce.string().optional(),
till: z.coerce.string().optional(),
limit: z.coerce.number().optional(),
title: z.coerce.string().optional(),
});

interface Props {
resourcesFilter: ReseourcesFilter;
searchParams: SearchParams;
}

export const Resources = async ({ resourcesFilter }: Props) => {
export const Resources = async ({ searchParams }: Props) => {
const resourcesFilter = reseourcesFilterSchema.parse(searchParams);

const title = resourcesFilter.title;
const from = resourcesFilter.from;
const till = resourcesFilter.till;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
LikedResources,
Resource,
} from '../../../../../lib/resources';
import { ReseourcesFilter } from '../../../page';
import { ReseourcesFilter } from '../../Resources';
import { ClearAllButton } from './ClearAllButton';
import { DownloadButton } from './DownloadButton/DownloadButton';
import { ResourcesListTop } from './ResourcesListTop';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
getResourcesCached,
getTopicsCached,
} from '../../../../lib/cache';
import { ReseourcesFilter } from '../../page';
import { ReseourcesFilter } from '../Resources';
import { ResourcesFilter } from './ResourcesFilter/ResourcesFilter';
import { ResourcesList } from './ResourcesList/ResourcesList';
import { ResourcesTableProvider } from './ResourcesTableProvider';
Expand Down
2 changes: 1 addition & 1 deletion apps/resources/app/resources/Suggestion/SuggestionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
inputStyles,
} from '../../../components/ForrestSection/ForrestSection';
import { useZodForm } from '../../../hooks/useZodForm';
import { useAction } from '../../../lib/actions/useAction';
import { useAction } from '../../../lib/serverActions/client';
import { submit } from './actions';
import { SuggestionFormSchema, suggestionFormSchema } from './schemas';

Expand Down
Loading

1 comment on commit de11c0d

@vercel
Copy link

@vercel vercel bot commented on de11c0d Oct 7, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.