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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@types/validator": "^13.15.3",
"@vercel/og": "^0.0.20",
"canvas-confetti": "^1.9.3",
"capture-node": "^2.2.0",
"chrono-node": "^2.7.6",
"classnames": "^2.3.1",
"easymde": "^2.18.0",
Expand Down
144 changes: 85 additions & 59 deletions apps/web/pages/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PageType, PageTypeToLabel } from "@changes-page/supabase/types/page";
import { PlusIcon, UserGroupIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { InferGetServerSidePropsType } from "next";
import type { InferGetServerSidePropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, type JSX } from "react";
import { type JSX, useEffect } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { PrimaryRouterButton } from "../../components/core/buttons.component";
import { notifyError } from "../../components/core/toast.component";
Expand All @@ -13,6 +13,7 @@ import AuthLayout from "../../components/layout/auth-layout.component";
import Page from "../../components/layout/page.component";
import Changelog from "../../components/marketing/changelog";
import { ROUTES } from "../../data/routes.data";
import { getPageScreenshotUrl } from "../../utils/capture";
import { getAppBaseURL } from "../../utils/helpers";
import { withSupabase } from "../../utils/supabase/withSupabase";
import { useUserData } from "../../utils/useUser";
Expand All @@ -33,16 +34,28 @@ export const getServerSideProps = withSupabase(async (_, { supabase }) => {
)
.order("updated_at", { ascending: false });

const screenshots = pages.map((page) =>
page
? getPageScreenshotUrl(
page.page_settings?.custom_domain
? `https://${page.page_settings.custom_domain}`
: `https://${page.url_slug}.changes.page`
)
: null
);

return {
props: {
pages: pages ?? [],
screenshots,
error,
},
};
});

export default function Pages({
pages,
screenshots,
error,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { billingDetails, user } = useUserData();
Expand Down Expand Up @@ -80,7 +93,7 @@ export default function Pages({
message="Get started by creating your first page."
buttonLink={
billingDetails?.has_active_subscription
? `/pages/new`
? "/pages/new"
: `/api/billing/redirect-to-checkout?return_url=${getAppBaseURL()}/pages`
}
buttonLabel={
Expand All @@ -107,69 +120,81 @@ export default function Pages({
{pages.length ? (
<div className="overflow-hidden shadow rounded-md bg-white dark:bg-gray-900 border dark:border-gray-800">
<ul className="divide-y divide-gray-200 dark:divide-gray-800">
{pages.map((page) => (
{pages.map((page, idx) => (
<li
key={page.id}
className="relative group hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-200"
>
<div className="flex items-center justify-between p-6">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<span
className={classNames(
page.type === PageType.announcements &&
"bg-blue-100 dark:bg-blue-700",
page.type === PageType.changelogs &&
"bg-teal-100 dark:bg-teal-700",
page.type === PageType.releases &&
"bg-rose-100 dark:bg-rose-700",
page.type === PageType.updates &&
"bg-amber-100 dark:bg-amber-700",
page.type === PageType.announcements &&
"text-blue-500 dark:text-blue-100",
page.type === PageType.changelogs &&
"text-teal-500 dark:text-teal-100",
page.type === PageType.releases &&
"text-rose-500 dark:text-rose-100",
page.type === PageType.updates &&
"text-amber-500 dark:text-amber-100",
"inline-flex px-2 py-1 text-xs font-bold rounded-md"
)}
>
{PageTypeToLabel[page.type]}
</span>
{page.teams && page.user_id !== user?.id && (
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
<UserGroupIcon className="h-4 w-4" />
<span className="text-xs font-medium">
Editor ({page.teams.name})
</span>
</div>
)}
</div>
<div>
<h3 className="text-lg font-bold tracking-tight text-gray-900 dark:text-white">
<Link
href={`${ROUTES.PAGES}/${page.id}`}
className="focus:outline-none hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors duration-200"
<div className="flex items-center gap-4">
{screenshots[idx] ? (
<div className="flex-shrink-0 hidden sm:block">
<img
className="w-48 h-18 object-cover rounded-md border border-gray-200 dark:border-gray-700"
src={screenshots[idx]}
alt={`Screenshot of ${page.title}`}
loading="lazy"
/>
</div>
) : null}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<span
className={classNames(
page.type === PageType.announcements &&
"bg-blue-100 dark:bg-blue-700",
page.type === PageType.changelogs &&
"bg-teal-100 dark:bg-teal-700",
page.type === PageType.releases &&
"bg-rose-100 dark:bg-rose-700",
page.type === PageType.updates &&
"bg-amber-100 dark:bg-amber-700",
page.type === PageType.announcements &&
"text-blue-500 dark:text-blue-100",
page.type === PageType.changelogs &&
"text-teal-500 dark:text-teal-100",
page.type === PageType.releases &&
"text-rose-500 dark:text-rose-100",
page.type === PageType.updates &&
"text-amber-500 dark:text-amber-100",
"inline-flex px-2 py-1 text-xs font-bold rounded-md"
)}
>
<span
className="absolute inset-0"
aria-hidden="true"
/>
{page.title}
</Link>
</h3>
{page.description && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{page.description}
{PageTypeToLabel[page.type]}
</span>
{page.teams && page.user_id !== user?.id && (
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-400">
<UserGroupIcon className="h-4 w-4" />
<span className="text-xs font-medium">
Editor ({page.teams.name})
</span>
</div>
)}
</div>
<div>
<h3 className="text-lg font-bold tracking-tight text-gray-900 dark:text-white">
<Link
href={`${ROUTES.PAGES}/${page.id}`}
className="focus:outline-none hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors duration-200"
>
<span
className="absolute inset-0"
aria-hidden="true"
/>
{page.title}
</Link>
</h3>
{page.description && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{page.description}
</p>
)}
<p className="mt-2 text-xs text-gray-400 dark:text-gray-500">
{page.page_settings?.custom_domain
? page.page_settings.custom_domain
: `${page.url_slug}.changes.page`}
</p>
)}
<p className="mt-2 text-xs text-gray-400 dark:text-gray-500">
{page.page_settings?.custom_domain
? page.page_settings.custom_domain
: `${page.url_slug}.changes.page`}
</p>
</div>
</div>
</div>
<div className="ml-4 flex-shrink-0">
Expand All @@ -178,6 +203,7 @@ export default function Pages({
fill="currentColor"
viewBox="0 0 20 20"
>
<title>Go to page</title>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
Expand Down
14 changes: 14 additions & 0 deletions apps/web/utils/capture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Capture } from "capture-node";

export function getPageScreenshotUrl(url: string) {
const capture = new Capture(
process.env.CAPTURE_API_KEY,
process.env.CAPTURE_API_SECRET
);

return capture.buildImageUrl(url, {
vw: 1280,
vh: 720,
scaleFactor: 1.5,
});
}
35 changes: 35 additions & 0 deletions pnpm-lock.yaml

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