{showBackButton && (
-
-
- }
- onClick={() => setOpenDeletePage(true)}
- />
-
+ {isPageOwner && (
+
+
+ }
+ onClick={() => setOpenDeletePage(true)}
+ />
+
+ )}
>
}
>
diff --git a/apps/web/pages/pages/[page_id]/new.tsx b/apps/web/pages/pages/[page_id]/new.tsx
index fecb9c5..89e9dc1 100644
--- a/apps/web/pages/pages/[page_id]/new.tsx
+++ b/apps/web/pages/pages/[page_id]/new.tsx
@@ -13,14 +13,12 @@ import { ROUTES } from "../../../data/routes.data";
import { NewPostSchema } from "../../../data/schema";
import { track } from "../../../utils/analytics";
import { httpPost } from "../../../utils/http";
-import { getSupabaseServerClient } from "../../../utils/supabase/supabase-admin";
import { createOrRetrievePageSettings } from "../../../utils/useDatabase";
-export async function getServerSideProps({ params, req, res }) {
+export async function getServerSideProps({ params }) {
const { page_id } = params;
- const { user } = await getSupabaseServerClient({ req, res });
- const settings = await createOrRetrievePageSettings(user.id, String(page_id));
+ const settings = await createOrRetrievePageSettings(String(page_id));
return {
props: {
diff --git a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx
index abe7c67..0e4cb0a 100644
--- a/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx
+++ b/apps/web/pages/pages/[page_id]/settings/[activeTab].tsx
@@ -24,9 +24,9 @@ const IntegrationsSettings = dynamic(
export async function getServerSideProps({ req, res, params }) {
const { page_id } = params;
- const { user, supabase } = await getSupabaseServerClient({ req, res });
+ const { supabase } = await getSupabaseServerClient({ req, res });
const page = await getPage(supabase, page_id);
- const settings = await createOrRetrievePageSettings(user.id, String(page_id));
+ const settings = await createOrRetrievePageSettings(String(page_id));
return {
props: {
diff --git a/apps/web/pages/pages/index.tsx b/apps/web/pages/pages/index.tsx
index 84166ca..56677d4 100644
--- a/apps/web/pages/pages/index.tsx
+++ b/apps/web/pages/pages/index.tsx
@@ -1,5 +1,5 @@
import { PageType, PageTypeToLabel } from "@changes-page/supabase/types/page";
-import { PlusIcon } from "@heroicons/react/solid";
+import { PlusIcon, UserGroupIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { useRouter } from "next/router";
@@ -21,7 +21,14 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { data: pages, error } = await supabase
.from("pages")
- .select("*")
+ .select(
+ `*,
+ teams (
+ id,
+ name
+ )
+ `
+ )
.order("updated_at", { ascending: false });
return {
@@ -36,7 +43,7 @@ export default function Pages({
pages,
error,
}: InferGetServerSidePropsType
) {
- const { billingDetails } = useUserData();
+ const { billingDetails, user } = useUserData();
const router = useRouter();
useHotkeys("n", () => router.push(ROUTES.NEW_PAGE), [router]);
@@ -52,7 +59,7 @@ export default function Pages({
title={"My Pages"}
buttons={
}
@@ -64,10 +71,10 @@ export default function Pages({
>
{billingDetails?.has_active_subscription ? : null}
-
+
{!pages.length && (
+
{pages.map((page) => (
{PageTypeToLabel[page.type]}
@@ -144,17 +149,17 @@ export default function Pages({
-
+ {page.teams && page.user_id !== user?.id ? (
+
+
+
+ Editor ({page.teams.name})
+
+
+ ) : null}
))}
@@ -167,4 +172,3 @@ export default function Pages({
}
Pages.getLayout = (page: JSX.Element) =>
{page};
-Pages.getLayout = (page: JSX.Element) =>
{page};
diff --git a/apps/web/pages/teams/index.tsx b/apps/web/pages/teams/index.tsx
new file mode 100644
index 0000000..8bb95e2
--- /dev/null
+++ b/apps/web/pages/teams/index.tsx
@@ -0,0 +1,629 @@
+import { IPage } from "@changes-page/supabase/types/page";
+import { SpinnerWithSpacing } from "@changes-page/ui";
+import {
+ CheckCircleIcon,
+ OfficeBuildingIcon,
+ PlusIcon,
+ UserIcon,
+} from "@heroicons/react/solid";
+import { useRouter } from "next/router";
+import { useCallback, useEffect, useState } from "react";
+import toast from "react-hot-toast";
+import { useHotkeys } from "react-hotkeys-hook";
+import {
+ PrimaryButton,
+ SecondaryButton,
+} from "../../components/core/buttons.component";
+import { notifyError } from "../../components/core/toast.component";
+import ConfirmDeleteDialog from "../../components/dialogs/confirm-delete-dialog.component";
+import ManageTeamDialog from "../../components/dialogs/manage-team-dialog.component";
+import { EntityEmptyState } from "../../components/entity/empty-state";
+import AuthLayout from "../../components/layout/auth-layout.component";
+import Page from "../../components/layout/page.component";
+import Changelog from "../../components/marketing/changelog";
+import MemeberDetails from "../../components/teams/memeber-details";
+import { track } from "../../utils/analytics";
+import { getAppBaseURL } from "../../utils/helpers";
+import { httpPost } from "../../utils/http";
+import { useUserData } from "../../utils/useUser";
+
+export default function Teams() {
+ const { billingDetails } = useUserData();
+ const router = useRouter();
+ const { user, supabase } = useUserData();
+
+ const [teams, setTeams] = useState([]);
+ const [showTeamModal, setShowTeamModal] = useState(false);
+ const [selectedTeam, setSelectedTeam] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [teamToDelete, setTeamToDelete] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ const [pages, setPages] = useState([]);
+ const [showAssignPage, setShowAssignPage] = useState
(null);
+ const [selectedPage, setSelectedPage] = useState(null);
+ const [assigningPage, setAssigningPage] = useState(false);
+
+ const fetchData = useCallback(async () => {
+ setIsLoading(true);
+
+ const [
+ { data: pages, error: pagesError },
+ { data: teams, error: teamsError },
+ ] = await Promise.all([
+ supabase.from("pages").select("*"),
+ supabase
+ .from("teams")
+ .select(
+ `
+ *,
+ pages (
+ id,
+ title,
+ created_at
+ ),
+ team_members (
+ id,
+ user_id,
+ role
+ ),
+ team_invitations(
+ id,
+ email,
+ role,
+ created_at
+ )
+ `
+ )
+ .eq("team_invitations.status", "pending"),
+ ]);
+
+ setPages(pages ?? []);
+ setTeams(teams ?? []);
+
+ if (pagesError) {
+ notifyError("Failed to fetch pages");
+ }
+
+ if (teamsError) {
+ notifyError("Failed to fetch teams");
+ }
+
+ setIsLoading(false);
+ }, [supabase]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ useHotkeys(
+ "n",
+ () => {
+ setSelectedTeam(null);
+ setShowTeamModal(true);
+ },
+ [router]
+ );
+
+ const deleteTeam = async (teamId: string) => {
+ setIsDeleting(true);
+ const { error } = await supabase
+ .from("teams")
+ .delete()
+ .match({ id: teamId });
+
+ if (error) {
+ notifyError("Failed to delete team");
+ return;
+ }
+
+ track("DeleteTeam");
+ fetchData();
+ setTeamToDelete(null);
+ setIsDeleting(false);
+ };
+
+ const handleAssignPage = async (teamId: string, pageId: string) => {
+ setAssigningPage(true);
+
+ await supabase
+ .from("pages")
+ .update({
+ team_id: teamId,
+ })
+ .match({ id: pageId });
+
+ track("AssignPageToTeam");
+ fetchData();
+ setAssigningPage(false);
+ setShowAssignPage(null);
+ setSelectedPage(null);
+ };
+
+ const handleRemovePage = async (pageId: string) => {
+ await supabase
+ .from("pages")
+ .update({
+ team_id: null,
+ })
+ .match({ id: pageId });
+
+ track("RemovePageFromTeam");
+ fetchData();
+ };
+
+ const handleRemoveMember = async (teamId: string, userId: string) => {
+ await supabase.from("team_members").delete().match({
+ team_id: teamId,
+ user_id: userId,
+ });
+
+ track("RemoveTeamMember");
+ fetchData();
+ };
+
+ const handleRevokeInvite = async (inviteId: string) => {
+ await supabase.from("team_invitations").delete().match({ id: inviteId });
+
+ track("RevokeTeamInvitation");
+ fetchData();
+ };
+
+ const handleAcceptInvite = async (inviteId: string) => {
+ await toast.promise(
+ httpPost({
+ url: "/api/teams/invite/accept",
+ data: {
+ invite_id: inviteId,
+ },
+ }),
+ {
+ loading: "Joining team...",
+ success: "Joined team",
+ error: "Failed to join team",
+ }
+ );
+
+ track("AcceptTeamInvitation");
+ fetchData();
+ };
+
+ const handleLeaveTeam = async (teamId: string) => {
+ if (confirm("Are you sure you want to leave this team?")) {
+ await supabase.from("team_members").delete().match({
+ team_id: teamId,
+ user_id: user?.id,
+ });
+
+ track("LeaveTeam");
+ fetchData();
+ }
+ };
+
+ const handleInviteTeamMember = async (teamId: string) => {
+ const email = prompt("Enter email address");
+ if (!email) {
+ return;
+ }
+
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ notifyError("Invalid email address");
+ return;
+ }
+
+ await toast.promise(
+ httpPost({
+ url: "/api/teams/invite",
+ data: {
+ team_id: teamId,
+ email,
+ },
+ }),
+ {
+ loading: "Sending invitation...",
+ success: "Invitation sent",
+ error: "Failed to send invitation",
+ }
+ );
+
+ fetchData();
+ };
+
+ return (
+ <>
+ setShowTeamModal(true)}
+ icon={
+
+ }
+ upgradeRequired={!billingDetails?.has_active_subscription}
+ />
+ }
+ >
+ {billingDetails?.has_active_subscription ? : null}
+
+
+ {isLoading ? (
+
+ ) : !teams.length ? (
+
{
+ if (!billingDetails?.has_active_subscription) {
+ router.push(
+ `/api/billing/redirect-to-checkout?return_url=${getAppBaseURL()}/teams`
+ );
+ return;
+ }
+
+ setShowTeamModal(true);
+ }}
+ />
+ ) : (
+
+ {teams.map((team) =>
+ team.owner_id === user?.id ? (
+
+
+
+ {team.image ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {team.name}
+
+
+
+
+
+
+
+
+
+
+
+
-
+ Pages
+
+
-
+
+ {team.pages?.length > 0 ? (
+
+ {team.pages?.map(
+ (page: Pick) => (
+ -
+
+
+
+
+ {page.title}
+
+
+
+
+
+
+
+ )
+ )}
+
+ ) : (
+
+ No pages yet, add one to share with your team.
+
+ Once shared, all team members will have
+ complete access to the page.
+
+ )}
+
+
+ {showAssignPage === team.id ? (
+
+
+
+
+ handleAssignPage(team.id, selectedPage)
+ }
+ />
+
+ setShowAssignPage(null)}
+ />
+
+ ) : (
+
setShowAssignPage(team.id)}
+ />
+ )}
+
+
+
+
+
-
+ Members
+
+ {team.team_members?.length > 0 ? (
+
-
+
+ {team.team_members?.map((member) => (
+ -
+
+
+
+
+
+ {member.role}
+
+
+
+
+
+ ))}
+
+
+ ) : (
+
-
+ No members yet
+
+ )}
+
+
-
+ Invites
+
+
-
+ {team.team_invitations?.length > 0 ? (
+
+ ) : null}
+
+ handleInviteTeamMember(team.id)}
+ />
+
+
+
+
+
+ ) : (
+
+
+
+ {team.image ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {team.team_members?.[0]?.role
+ ? `${
+ team.name
+ } (${team.team_members[0].role.toUpperCase()})`
+ : `You've been invited to join ${team.name} team, would you like to join?`}
+
+
+ {team.team_invitations?.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ )
+ )}
+
+ )}
+
+
+
+ {
+ fetchData();
+ setShowTeamModal(false);
+ setSelectedTeam(null);
+ }}
+ onCancel={() => {
+ setSelectedTeam(null);
+ }}
+ />
+
+ setTeamToDelete(null)}
+ deleteCallback={() => deleteTeam(teamToDelete?.id)}
+ itemName={teamToDelete?.name}
+ highRiskAction
+ riskVerificationText={teamToDelete?.name}
+ processing={isDeleting}
+ />
+ >
+ );
+}
+
+Teams.getLayout = (page: JSX.Element) => {page};
diff --git a/apps/web/styles/global.css b/apps/web/styles/global.css
index 590aa89..61d7092 100644
--- a/apps/web/styles/global.css
+++ b/apps/web/styles/global.css
@@ -89,3 +89,16 @@ body {
border-color: rgba(245, 245, 245, 0.1) !important;
}
}
+
+#nprogress .bar {
+ background: #4f46e5 !important;
+}
+
+#nprogress .peg {
+ box-shadow: 0 0 10px #4f46e5, 0 0 5px #4f46e5;
+}
+
+#nprogress .spinner-icon {
+ border-top-color: #4f46e5;
+ border-left-color: #4f46e5;
+}
\ No newline at end of file
diff --git a/apps/web/utils/hooks/usePageSettings.ts b/apps/web/utils/hooks/usePageSettings.ts
index ba9ea54..c759d7e 100644
--- a/apps/web/utils/hooks/usePageSettings.ts
+++ b/apps/web/utils/hooks/usePageSettings.ts
@@ -9,7 +9,7 @@ import { httpGet } from "../http";
import { useUserData } from "../useUser";
export default function usePageSettings(pageId: string, prefetch = true) {
- const { supabase } = useUserData();
+ const { supabase, user } = useUserData();
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState(null);
@@ -22,6 +22,13 @@ export default function usePageSettings(pageId: string, prefetch = true) {
.match({ page_id: pageId })
.select();
+ await supabase.from("page_audit_logs").insert({
+ page_id: pageId,
+ actor_id: user.id,
+ action: "Updated Page Settings",
+ changes: payload,
+ });
+
if (settings.length) {
setSettings(settings[0]);
}
diff --git a/apps/web/utils/revalidate.ts b/apps/web/utils/revalidate.ts
deleted file mode 100644
index 5ad6e80..0000000
--- a/apps/web/utils/revalidate.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { httpPost } from "./http";
-
-export const revalidatePage = async (path: string) => {
- try {
- const revalidateEndpoint =
- process.env.REVALIDATE_ENDPOINT || "https://hey.changes.page";
-
- const result = await httpPost({
- url: `${revalidateEndpoint}/api/revalidate?secret=${process.env.REVALIDATE_TOKEN}`,
- data: { path },
- });
-
- console.log("revalidatePage: Result:", path, result);
- } catch (error) {
- console.error("revalidatePage failed: Error:", error);
- }
-};
diff --git a/apps/web/utils/useDatabase.ts b/apps/web/utils/useDatabase.ts
index babb1a2..b1646da 100644
--- a/apps/web/utils/useDatabase.ts
+++ b/apps/web/utils/useDatabase.ts
@@ -207,14 +207,12 @@ export const updateSubscriptionUsage = async (
};
export const createOrRetrievePageSettings = async (
- user_id: string,
page_id: string
) => {
const { data: pageSettings } = await supabaseAdmin
.from("page_settings")
.select("*")
.eq("page_id", page_id)
- .eq("user_id", user_id)
.single();
if (pageSettings) return pageSettings;
@@ -222,7 +220,7 @@ export const createOrRetrievePageSettings = async (
const { data: newPageSettings, error: createPageSettingsError } =
await supabaseAdmin
.from("page_settings")
- .insert([{ user_id, page_id }])
+ .insert([{ page_id }])
.select();
if (createPageSettingsError) throw createPageSettingsError;
@@ -376,3 +374,4 @@ export async function getPageAnalytics(
return data as unknown as IAnalyticsData[];
}
+
diff --git a/apps/web/utils/useSSR.ts b/apps/web/utils/useSSR.ts
index f8f4a29..34d723e 100644
--- a/apps/web/utils/useSSR.ts
+++ b/apps/web/utils/useSSR.ts
@@ -3,7 +3,7 @@ import { SupabaseClient } from "@supabase/auth-helpers-nextjs";
export async function getPage(supabase: SupabaseClient, id: string) {
const { data: page } = await supabase
.from("pages")
- .select("id,title,type,description,url_slug")
+ .select("id,title,type,description,url_slug,user_id")
.eq("id", id)
.maybeSingle()
.throwOnError();
diff --git a/packages/supabase/cron.sql b/packages/supabase/cron.sql
index c07facf..b29f472 100644
--- a/packages/supabase/cron.sql
+++ b/packages/supabase/cron.sql
@@ -8,4 +8,7 @@ SELECT cron.schedule('*/15 * * * *', $$delete from page_email_subscribers where
SELECT cron.schedule('0 0 * * *', $$delete from page_views where created_at < now() - interval '1 month'$$);
-- Remove all cron job logs older than 1 month
-select cron.schedule('0 0 * * *', $$delete from cron.job_run_details where end_time < now() - interval '7 days'$$);
\ No newline at end of file
+select cron.schedule('0 0 * * *', $$delete from cron.job_run_details where end_time < now() - interval '7 days'$$);
+
+-- Remove all expired team invitations
+select cron.schedule('*/15 * * * *', $$delete from team_invitations where expires_at < now() and status = 'pending'$$);
diff --git a/packages/supabase/migrations/16_teams.sql b/packages/supabase/migrations/16_teams.sql
new file mode 100644
index 0000000..6fa815d
--- /dev/null
+++ b/packages/supabase/migrations/16_teams.sql
@@ -0,0 +1,113 @@
+-- TEAMS table
+create table teams (
+ id uuid default uuid_generate_v4() primary key,
+ owner_id uuid references users not null,
+ name text not null,
+ image text,
+ metadata jsonb,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null,
+ updated_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+alter table teams add constraint name_length check (length(name) >= 3);
+
+alter table teams enable row level security;
+create policy "Can insert teams." on teams for insert with check (auth.uid() = owner_id);
+create policy "Can view own teams." on teams for select using (auth.uid() = owner_id);
+create policy "Can update own teams." on teams for update using (auth.uid() = owner_id);
+create policy "Can delete own teams." on teams for delete using (auth.uid() = owner_id);
+
+CREATE TRIGGER set_timestamp
+BEFORE UPDATE ON teams
+FOR EACH ROW
+EXECUTE PROCEDURE trigger_set_timestamp();
+
+-- TEAM MEMBERS table
+create table team_members (
+ id uuid default uuid_generate_v4() primary key,
+ team_id uuid references teams not null,
+ user_id uuid references users not null,
+ role text not null,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null,
+ updated_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+alter table team_members add constraint unique_team_user unique (team_id, user_id);
+
+alter table team_members enable row level security;
+create policy "Owner can view own team members." on team_members for select using (team_id in (select id from teams where owner_id = auth.uid()));
+create policy "Owner can delete own team members." on team_members for delete using (team_id in (select id from teams where owner_id = auth.uid()));
+create policy "Team members can leave their team." on team_members for delete using (user_id = auth.uid());
+
+CREATE TRIGGER set_timestamp
+BEFORE UPDATE ON team_members
+FOR EACH ROW
+EXECUTE PROCEDURE trigger_set_timestamp();
+
+-- TEAM INVITATIONS table
+create table team_invitations (
+ id uuid default uuid_generate_v4() primary key,
+ team_id uuid references teams not null,
+ inviter_id uuid references users not null,
+ email text not null,
+ role text not null,
+ status text not null,
+ expires_at timestamp with time zone not null default timezone('utc'::text, now() + interval '1 day'),
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null,
+ updated_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+alter table team_invitations add constraint unique_email_team_id unique (email, team_id);
+
+alter table team_invitations enable row level security;
+create policy "Can view own team invitations." on team_invitations for select using (auth.uid() = inviter_id);
+create policy "Can delete own team invitations." on team_invitations for delete using (auth.uid() = inviter_id);
+create policy "Can view team invitations." on team_invitations for select using (auth.email() = email);
+create policy "Invited users can view teams." on teams for select using (id in (select team_id from team_invitations where email = auth.email()));
+
+CREATE TRIGGER set_timestamp
+BEFORE UPDATE ON team_invitations
+FOR EACH ROW
+EXECUTE PROCEDURE trigger_set_timestamp();
+
+-- Helper function to check if a user is a member of a team
+create or replace function is_team_member(tid uuid, uid uuid) returns boolean as $$
+begin
+ return exists (select 1 from team_members where team_id = tid and user_id = uid);
+end;
+$$ language plpgsql security definer;
+
+-- Policies using the helper function
+create policy "Team members can view their membership." on team_members for select using (is_team_member(team_id, auth.uid()));
+create policy "Members can view teams." on teams for select using (is_team_member(id, auth.uid()));
+
+-- Alter pages table to add team_id
+alter table pages add column team_id uuid references teams on delete set null;
+
+-- Teams members can view pages in their team and posts in those pages
+create policy "Can view pages in own team." on pages for select using (team_id in (select team_id from team_members where user_id = auth.uid()));
+
+-- Update page_settings
+alter policy "Can view own page_settings." on page_settings using (page_id in (select id from pages));
+alter policy "Can update own page_settings." on page_settings using (page_id in (select id from pages));
+drop policy "Can delete own page_settings." on page_settings;
+
+alter table page_settings drop column user_id;
+
+-- Update posts
+alter policy "Can view own posts." on posts using (page_id in (select id from pages));
+alter policy "Can update own posts." on posts using (page_id in (select id from pages));
+alter policy "Can delete own posts." on posts using (page_id in (select id from pages));
+
+-- Audit logs table
+create table page_audit_logs (
+ id uuid default uuid_generate_v4() primary key,
+ page_id uuid references pages not null,
+ actor_id uuid references users not null,
+ action text not null,
+ changes jsonb,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+alter table page_audit_logs enable row level security;
+create policy "Can insert page audit logs." on page_audit_logs for insert with check (actor_id = auth.uid());
\ No newline at end of file
diff --git a/packages/supabase/types/index.ts b/packages/supabase/types/index.ts
index 285dd59..6b653b2 100644
--- a/packages/supabase/types/index.ts
+++ b/packages/supabase/types/index.ts
@@ -9,6 +9,48 @@ export type Json =
export type Database = {
public: {
Tables: {
+ page_audit_logs: {
+ Row: {
+ action: string
+ actor_id: string
+ changes: Json | null
+ created_at: string
+ id: string
+ page_id: string
+ }
+ Insert: {
+ action: string
+ actor_id: string
+ changes?: Json | null
+ created_at?: string
+ id?: string
+ page_id: string
+ }
+ Update: {
+ action?: string
+ actor_id?: string
+ changes?: Json | null
+ created_at?: string
+ id?: string
+ page_id?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "page_audit_logs_actor_id_fkey"
+ columns: ["actor_id"]
+ isOneToOne: false
+ referencedRelation: "users"
+ referencedColumns: ["id"]
+ },
+ {
+ foreignKeyName: "page_audit_logs_page_id_fkey"
+ columns: ["page_id"]
+ isOneToOne: false
+ referencedRelation: "pages"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
page_email_subscribers: {
Row: {
created_at: string
@@ -78,7 +120,6 @@ export type Database = {
tiktok_url: string | null
twitter_url: string | null
updated_at: string
- user_id: string
whitelabel: boolean
youtube_url: string | null
}
@@ -109,7 +150,6 @@ export type Database = {
tiktok_url?: string | null
twitter_url?: string | null
updated_at?: string
- user_id: string
whitelabel?: boolean
youtube_url?: string | null
}
@@ -140,7 +180,6 @@ export type Database = {
tiktok_url?: string | null
twitter_url?: string | null
updated_at?: string
- user_id?: string
whitelabel?: boolean
youtube_url?: string | null
}
@@ -203,6 +242,7 @@ export type Database = {
created_at: string
description: string | null
id: string
+ team_id: string | null
title: string
type: Database["public"]["Enums"]["page_type"]
updated_at: string
@@ -213,6 +253,7 @@ export type Database = {
created_at?: string
description?: string | null
id?: string
+ team_id?: string | null
title: string
type: Database["public"]["Enums"]["page_type"]
updated_at?: string
@@ -223,13 +264,22 @@ export type Database = {
created_at?: string
description?: string | null
id?: string
+ team_id?: string | null
title?: string
type?: Database["public"]["Enums"]["page_type"]
updated_at?: string
url_slug?: string | null
user_id?: string
}
- Relationships: []
+ Relationships: [
+ {
+ foreignKeyName: "pages_team_id_fkey"
+ columns: ["team_id"]
+ isOneToOne: false
+ referencedRelation: "teams"
+ referencedColumns: ["id"]
+ },
+ ]
}
post_reactions: {
Row: {
@@ -337,6 +387,137 @@ export type Database = {
},
]
}
+ team_invitations: {
+ Row: {
+ created_at: string
+ email: string
+ expires_at: string
+ id: string
+ inviter_id: string
+ role: string
+ status: string
+ team_id: string
+ updated_at: string
+ }
+ Insert: {
+ created_at?: string
+ email: string
+ expires_at?: string
+ id?: string
+ inviter_id: string
+ role: string
+ status: string
+ team_id: string
+ updated_at?: string
+ }
+ Update: {
+ created_at?: string
+ email?: string
+ expires_at?: string
+ id?: string
+ inviter_id?: string
+ role?: string
+ status?: string
+ team_id?: string
+ updated_at?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "team_invitations_inviter_id_fkey"
+ columns: ["inviter_id"]
+ isOneToOne: false
+ referencedRelation: "users"
+ referencedColumns: ["id"]
+ },
+ {
+ foreignKeyName: "team_invitations_team_id_fkey"
+ columns: ["team_id"]
+ isOneToOne: false
+ referencedRelation: "teams"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ team_members: {
+ Row: {
+ created_at: string
+ id: string
+ role: string
+ team_id: string
+ updated_at: string
+ user_id: string
+ }
+ Insert: {
+ created_at?: string
+ id?: string
+ role: string
+ team_id: string
+ updated_at?: string
+ user_id: string
+ }
+ Update: {
+ created_at?: string
+ id?: string
+ role?: string
+ team_id?: string
+ updated_at?: string
+ user_id?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "team_members_team_id_fkey"
+ columns: ["team_id"]
+ isOneToOne: false
+ referencedRelation: "teams"
+ referencedColumns: ["id"]
+ },
+ {
+ foreignKeyName: "team_members_user_id_fkey"
+ columns: ["user_id"]
+ isOneToOne: false
+ referencedRelation: "users"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
+ teams: {
+ Row: {
+ created_at: string
+ id: string
+ image: string | null
+ metadata: Json | null
+ name: string
+ owner_id: string
+ updated_at: string
+ }
+ Insert: {
+ created_at?: string
+ id?: string
+ image?: string | null
+ metadata?: Json | null
+ name: string
+ owner_id: string
+ updated_at?: string
+ }
+ Update: {
+ created_at?: string
+ id?: string
+ image?: string | null
+ metadata?: Json | null
+ name?: string
+ owner_id?: string
+ updated_at?: string
+ }
+ Relationships: [
+ {
+ foreignKeyName: "teams_owner_id_fkey"
+ columns: ["owner_id"]
+ isOneToOne: false
+ referencedRelation: "users"
+ referencedColumns: ["id"]
+ },
+ ]
+ }
users: {
Row: {
avatar_url: string | null
@@ -378,6 +559,13 @@ export type Database = {
}
Returns: boolean
}
+ is_team_member: {
+ Args: {
+ tid: string
+ uid: string
+ }
+ Returns: boolean
+ }
page_view_browsers: {
Args: {
pageid: string
diff --git a/packages/supabase/types/page.ts b/packages/supabase/types/page.ts
index 1c38b3b..6b68040 100644
--- a/packages/supabase/types/page.ts
+++ b/packages/supabase/types/page.ts
@@ -7,6 +7,10 @@ export type IPost = Database["public"]["Tables"]["posts"]["Row"];
export type IPageEmailSubscriber =
Database["public"]["Tables"]["page_email_subscribers"]["Row"];
export type IPageView = Database["public"]["Tables"]["page_views"]["Row"];
+export type ITeam = Database["public"]["Tables"]["teams"]["Row"];
+export type ITeamMember = Database["public"]["Tables"]["team_members"]["Row"];
+export type ITeamInvitation =
+ Database["public"]["Tables"]["team_invitations"]["Row"];
export enum PageType {
changelogs = "changelogs",
@@ -61,3 +65,11 @@ export const PostStatusToLabel: {
export const tagColors = ["red", "amber", "teal", "sky", "pink"];
export const URL_SLUG_REGEX = new RegExp("^[a-zA-Z0-9]([w-]*[a-zA-Z0-9])*$");
+
+export type IReactions = {
+ thumbs_up?: number;
+ thumbs_down?: number;
+ rocket?: number;
+ sad?: number;
+ heart?: number;
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 151d915..16b661a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -263,6 +263,9 @@ importers:
next-sitemap:
specifier: ^4.2.3
version: 4.2.3(next@14.1.0)
+ nprogress:
+ specifier: ^0.2.0
+ version: 0.2.0
openai:
specifier: ^3.2.1
version: 3.3.0
@@ -1592,7 +1595,6 @@ packages:
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
- dev: false
/@typescript-eslint/parser@6.19.1(eslint@7.32.0)(typescript@5.5.4):
resolution: {integrity: sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==}
@@ -1613,6 +1615,7 @@ packages:
typescript: 5.5.4
transitivePeerDependencies:
- supports-color
+ dev: true
/@typescript-eslint/scope-manager@6.19.1:
resolution: {integrity: sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==}
@@ -1645,7 +1648,6 @@ packages:
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
- dev: false
/@typescript-eslint/typescript-estree@6.19.1(typescript@5.5.4):
resolution: {integrity: sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==}
@@ -1667,6 +1669,7 @@ packages:
typescript: 5.5.4
transitivePeerDependencies:
- supports-color
+ dev: true
/@typescript-eslint/visitor-keys@6.19.1:
resolution: {integrity: sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==}
@@ -2657,7 +2660,7 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
- '@typescript-eslint/parser': 6.19.1(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 6.19.1(eslint@7.32.0)(typescript@4.9.5)
debug: 3.2.7
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
@@ -2675,7 +2678,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
- '@typescript-eslint/parser': 6.19.1(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 6.19.1(eslint@7.32.0)(typescript@4.9.5)
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.3
array.prototype.flat: 1.3.2
@@ -4903,6 +4906,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /nprogress@0.2.0:
+ resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
+ dev: false
+
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -6139,7 +6146,6 @@ packages:
typescript: '>=4.2.0'
dependencies:
typescript: 4.9.5
- dev: false
/ts-api-utils@1.0.3(typescript@5.5.4):
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
@@ -6148,6 +6154,7 @@ packages:
typescript: '>=4.2.0'
dependencies:
typescript: 5.5.4
+ dev: true
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@@ -6243,6 +6250,7 @@ packages:
resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
engines: {node: '>=14.17'}
hasBin: true
+ dev: true
/typo-js@1.2.3:
resolution: {integrity: sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA==}