Skip to content

Commit

Permalink
Remember the last visited community (#912)
Browse files Browse the repository at this point in the history
* Remember the last visited community

* Set cookie client side

Co-authored-by: Thomas F. K. Jorna <[email protected]>

* Remove now unused server action

* Update logout in test to account for new menu

---------

Co-authored-by: Thomas F. K. Jorna <[email protected]>
  • Loading branch information
kalilsn and tefkah authored Jan 23, 2025
1 parent 5eebab0 commit e3319e9
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 17 deletions.
7 changes: 6 additions & 1 deletion core/app/(user)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants";
import { getLoginData } from "~/lib/authentication/loginData";
import LoginForm from "./LoginForm";

Expand All @@ -14,9 +16,12 @@ export default async function Login({

if (user?.id) {
const firstSlug = user.memberships[0]?.community?.slug;
const cookieStore = await cookies();
const lastVisited = cookieStore.get(LAST_VISITED_COOKIE);
const communitySlug = lastVisited?.value ?? firstSlug;

if (firstSlug) {
redirect(`/c/${firstSlug}/stages`);
redirect(`/c/${communitySlug}/stages`);
}

redirect("/settings");
Expand Down
9 changes: 5 additions & 4 deletions core/app/c/[communitySlug]/CommunitySwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "ui/dropdown-menu";
import { ChevronDown, ChevronsUpDown } from "ui/icon";
import { ChevronsUpDown } from "ui/icon";
import { SidebarMenuButton } from "ui/sidebar";
import { cn } from "utils";

Expand All @@ -25,16 +25,17 @@ const CommunitySwitcher: React.FC<Props> = function ({ community, availableCommu
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{/* <div className="flex cursor-pointer items-center rounded p-1 hover:bg-gray-200 md:p-2"> */}
<SidebarMenuButton className={`h-full group-data-[collapsible=icon]:!p-0 md:py-1`}>
<SidebarMenuButton
aria-label="Select a community"
className={`h-full group-data-[collapsible=icon]:!p-0 md:py-1`}
>
<Avatar className={avatarClasses}>
<AvatarImage src={community.avatar || undefined} />
<AvatarFallback>{community.name[0]}</AvatarFallback>
</Avatar>
<span className={textClasses}>{community.name}</span>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
{/* </div> */}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[--radix-popper-anchor-width] min-w-52" side="right">
{availableCommunities
Expand Down
9 changes: 8 additions & 1 deletion core/app/c/[communitySlug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { Metadata } from "next";
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";

import { Sidebar, SidebarProvider, SidebarTrigger } from "ui/sidebar";
import { SidebarProvider, SidebarTrigger } from "ui/sidebar";
import { cn } from "utils";

import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants";
import SetLastVisited from "~/app/components/LastVisitedCommunity/SetLastVisited";
import { CommunityProvider } from "~/app/components/providers/CommunityProvider";
import { getPageLoginData } from "~/lib/authentication/loginData";
import { getCommunityRole } from "~/lib/authentication/roles";
Expand Down Expand Up @@ -55,8 +57,13 @@ export default async function MainLayout(props: Props) {

const availableCommunities = user?.memberships.map((m) => m.community) ?? [];

const lastVisited = cookieStore.get(LAST_VISITED_COOKIE);

return (
<CommunityProvider community={community}>
{params.communitySlug !== lastVisited?.value && (
<SetLastVisited communitySlug={params.communitySlug} />
)}
<div className="flex min-h-screen flex-col md:flex-row">
<SidebarProvider defaultOpen={defaultOpen}>
<SideNav community={community} availableCommunities={availableCommunities} />
Expand Down
13 changes: 13 additions & 0 deletions core/app/components/LastVisitedCommunity/SetLastVisited.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { useEffect } from "react";

import { LAST_VISITED_COOKIE, LAST_VISITED_COOKIE_MAX_AGE } from "./constants";

export default function SetLastVisited({ communitySlug }: { communitySlug: string }) {
useEffect(() => {
document.cookie = `${LAST_VISITED_COOKIE}=${communitySlug}; path=/; max-age=${LAST_VISITED_COOKIE_MAX_AGE}`;
}, [communitySlug]);

return <></>;
}
2 changes: 2 additions & 0 deletions core/app/components/LastVisitedCommunity/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const LAST_VISITED_COOKIE = "lastVisitedCommunitySlug";
export const LAST_VISITED_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
11 changes: 7 additions & 4 deletions core/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants";
import { getPageLoginData } from "~/lib/authentication/loginData";

export default async function Page() {
const { user } = await getPageLoginData();

// if user and no commuhnmitiy, redirect to settings
if (!user) {
redirect("/login");
}

const memberSlug = user.memberships[0]?.community?.slug;
const cookieStore = await cookies();
const lastVisited = cookieStore.get(LAST_VISITED_COOKIE);
const communitySlug = lastVisited?.value ?? user.memberships[0]?.community?.slug;

if (!memberSlug) {
if (!communitySlug) {
redirect("/settings");
}

redirect(`/c/${memberSlug}/stages`);
redirect(`/c/${communitySlug}/stages`);
}
14 changes: 9 additions & 5 deletions core/lib/authentication/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { lucia, validateRequest } from "~/lib/authentication/lucia";
import { validatePassword } from "~/lib/authentication/password";
import { defineServerAction } from "~/lib/server/defineServerAction";
import { getUser, setUserPassword, updateUser } from "~/lib/server/user";
import { LAST_VISITED_COOKIE } from "../../app/components/LastVisitedCommunity/constants";
import * as Email from "../server/email";
import { invalidateTokensForUser } from "../server/token";
import { getLoginData } from "./loginData";
Expand All @@ -32,16 +33,19 @@ type LoginUser = Prettify<
const getUserWithPasswordHash = async (props: Parameters<typeof getUser>[0]) =>
getUser(props).select("users.passwordHash").executeTakeFirst();

function redirectUser(
async function redirectUser(
memberships?: (Omit<CommunityMemberships, "memberGroupId"> & {
community: Communities | null;
})[]
): never {
): Promise<never> {
if (!memberships?.length) {
redirect("/settings");
}
const cookieStore = await cookies();
const lastVisited = cookieStore.get(LAST_VISITED_COOKIE);
const communitySlug = lastVisited?.value ?? memberships[0].community?.slug;

redirect(`/c/${memberships[0].community?.slug}/stages`);
redirect(`/c/${communitySlug}/stages`);
}

export const loginWithPassword = defineServerAction(async function loginWithPassword(props: {
Expand Down Expand Up @@ -89,7 +93,7 @@ export const loginWithPassword = defineServerAction(async function loginWithPass
redirect(props.redirectTo);
}

redirectUser(user.memberships);
await redirectUser(user.memberships);
});

export const logout = defineServerAction(async function logout() {
Expand Down Expand Up @@ -283,5 +287,5 @@ export const signup = defineServerAction(async function signup(props: {
if (props.redirect) {
redirect(props.redirect);
}
redirectUser();
await redirectUser();
});
26 changes: 24 additions & 2 deletions core/playwright/login.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { expect, test } from "@playwright/test";

import { LoginPage } from "./fixtures/login-page";
import { inbucketClient, retryAction } from "./helpers";
import * as loginFlows from "./login.flows";
import { inbucketClient } from "./helpers";

test.describe("general auth", () => {
test("Login with invalid credentials", async ({ page }) => {
Expand Down Expand Up @@ -110,3 +109,26 @@ test.describe("Auth with lucia", () => {
await page.waitForURL(/\/c\/\w+\/stages/);
});
});

test("Last visited community is remembered", async ({ page }) => {
const unjournalRegex = new RegExp("/c/unjournal/");
const croccrocRegex = new RegExp("/c/croccroc/");
const loginPage = new LoginPage(page);

// Login and visit default community
await loginPage.goto();
await loginPage.loginAndWaitForNavigation("[email protected]", "pubpub-all");
await expect(page).toHaveURL(unjournalRegex);

// Switch communities and logout
await page.getByRole("button", { name: "Select a community" }).click();
await page.getByRole("menuitem", { name: "CrocCroc" }).click();
await page.waitForURL(croccrocRegex);
await page.getByRole("button", { name: "User menu" }).click();
await page.getByRole("button", { name: "Logout" }).click();

// Log back in and switched community should be remembered
await page.waitForURL("/login");
await loginPage.loginAndWaitForNavigation("[email protected]", "pubpub-all");
await expect(page).toHaveURL(croccrocRegex);
});

0 comments on commit e3319e9

Please sign in to comment.