Add packages/common for shared utilities
Create new @reviq/common package with environment-agnostic utilities: - Date formatting: formatDate, formatDateTime, formatLongDate, formatRelativeDate, formatRelativeTime - User utilities: getUserInitials, formatRole Consolidate date formatting from publisher-dashboard into shared package. All utilities include comprehensive test coverage with bun:test. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"@orpc/client": "^1.13.2",
|
||||
"@orpc/contract": "^1.13.2",
|
||||
"@reviq/api-contract": "workspace:*",
|
||||
"@reviq/common": "workspace:*",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@tanstack/svelte-query": "^6.0.14",
|
||||
"@tanstack/svelte-query-devtools": "^6.0.3",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Key, Pencil, Trash2 } from "@lucide/svelte";
|
||||
import { formatDate, formatRelativeTime } from "@reviq/common";
|
||||
import { useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { api } from "$lib/api/client";
|
||||
@@ -28,39 +29,6 @@ let deleteDialogOpen = $state(false);
|
||||
let selectedPasskey = $state<Passkey | null>(null);
|
||||
let isDeleting = $state(false);
|
||||
|
||||
function formatDate(date: Date | string): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
return d.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatRelativeTime(date: Date | string | null): string {
|
||||
if (!date) {
|
||||
return "Never";
|
||||
}
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return "Today";
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return "Yesterday";
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
}
|
||||
if (diffDays < 30) {
|
||||
return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
}
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
function openRename(passkey: Passkey) {
|
||||
selectedPasskey = passkey;
|
||||
renameDialogOpen = true;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import * as Sheet from "$lib/components/ui/sheet";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { getUserInitials } from "@reviq/common";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -24,31 +25,15 @@ const userQuery = createQuery(() => ({
|
||||
}));
|
||||
|
||||
const user = $derived(userQuery.data);
|
||||
|
||||
// Generate initials from display name or email
|
||||
const initials = $derived.by(() => {
|
||||
if (!user) {
|
||||
return "??";
|
||||
}
|
||||
if (user.displayName) {
|
||||
const parts = user.displayName.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
return (
|
||||
parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
).toUpperCase();
|
||||
}
|
||||
return user.displayName.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return user.email.slice(0, 2).toUpperCase();
|
||||
});
|
||||
const initials = $derived(getUserInitials(user));
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function handleNavClick() {
|
||||
function handleNavClick(): void {
|
||||
open = false;
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
async function handleSignOut(): Promise<void> {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { page } from "$app/stores";
|
||||
import { api } from "$lib/api/client";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { getUserInitials } from "@reviq/common";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -20,27 +21,11 @@ const userQuery = createQuery(() => ({
|
||||
}));
|
||||
|
||||
const user = $derived(userQuery.data);
|
||||
|
||||
// Generate initials from display name or email
|
||||
const initials = $derived.by(() => {
|
||||
if (!user) {
|
||||
return "??";
|
||||
}
|
||||
if (user.displayName) {
|
||||
const parts = user.displayName.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
return (
|
||||
parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
).toUpperCase();
|
||||
}
|
||||
return user.displayName.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return user.email.slice(0, 2).toUpperCase();
|
||||
});
|
||||
const initials = $derived(getUserInitials(user));
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function handleSignOut() {
|
||||
async function handleSignOut(): Promise<void> {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Button } from "$lib/components/ui/button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import * as Sheet from "$lib/components/ui/sheet";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { getUserInitials } from "@reviq/common";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -32,43 +33,17 @@ const userQuery = createQuery(() => ({
|
||||
}));
|
||||
|
||||
const user = $derived(userQuery.data);
|
||||
|
||||
// Generate initials from display name or email
|
||||
const initials = $derived.by(() => {
|
||||
if (!user) {
|
||||
return "??";
|
||||
}
|
||||
if (user.displayName) {
|
||||
const parts = user.displayName.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
return (
|
||||
parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
).toUpperCase();
|
||||
}
|
||||
return user.displayName.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return user.email.slice(0, 2).toUpperCase();
|
||||
});
|
||||
const initials = $derived(getUserInitials(user));
|
||||
|
||||
// Nav items depend on whether we're in an org context
|
||||
const navItems = $derived.by(() => {
|
||||
if (currentSlug) {
|
||||
// In org context - org-specific navigation
|
||||
return [
|
||||
{ icon: "home", href: `/dashboard/${currentSlug}`, label: "Home" },
|
||||
{
|
||||
icon: "chart",
|
||||
href: `/dashboard/${currentSlug}/performance`,
|
||||
label: "Performance",
|
||||
},
|
||||
{
|
||||
icon: "document",
|
||||
href: `/dashboard/${currentSlug}/reports`,
|
||||
label: "Reports",
|
||||
},
|
||||
{ icon: "chart", href: `/dashboard/${currentSlug}/performance`, label: "Performance" },
|
||||
{ icon: "document", href: `/dashboard/${currentSlug}/reports`, label: "Reports" },
|
||||
];
|
||||
}
|
||||
// Outside org context - general navigation
|
||||
return [
|
||||
{ icon: "home", href: "/", label: "Home" },
|
||||
{ icon: "building", href: "/dashboard", label: "Organizations" },
|
||||
@@ -77,11 +52,11 @@ const navItems = $derived.by(() => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function handleNavClick() {
|
||||
function handleNavClick(): void {
|
||||
open = false;
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
async function handleSignOut(): Promise<void> {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
queryClient.clear();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { getUserInitials } from "@reviq/common";
|
||||
|
||||
// Get optional org context (undefined outside org routes)
|
||||
const orgContext = getContext<{ currentUserRole: string | null } | undefined>(
|
||||
@@ -19,30 +20,13 @@ const userQuery = createQuery(() => ({
|
||||
}));
|
||||
|
||||
const user = $derived(userQuery.data);
|
||||
|
||||
// Generate initials from display name or email
|
||||
const initials = $derived.by(() => {
|
||||
if (!user) {
|
||||
return "??";
|
||||
}
|
||||
if (user.displayName) {
|
||||
const parts = user.displayName.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
return (
|
||||
parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
||||
).toUpperCase();
|
||||
}
|
||||
return user.displayName.slice(0, 2).toUpperCase();
|
||||
}
|
||||
return user.email.slice(0, 2).toUpperCase();
|
||||
});
|
||||
const initials = $derived(getUserInitials(user));
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function handleSignOut() {
|
||||
async function handleSignOut(): Promise<void> {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
// Clear all cached queries
|
||||
queryClient.clear();
|
||||
goto(resolve("/auth/login"));
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { Building2, Globe, Settings, Users } from "@lucide/svelte";
|
||||
import { Globe, Settings, Users } from "@lucide/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Date formatting utilities for consistent display across the app
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a date for display in tables and lists
|
||||
* Example: "Jan 15, 2024"
|
||||
*/
|
||||
export function formatDate(date: string | Date): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date with time for detailed views
|
||||
* Example: "Jan 15, 2024, 3:30 PM"
|
||||
*/
|
||||
export function formatDateTime(date: string | Date): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { formatDate, formatRelativeDate } from "@reviq/common";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -59,33 +60,6 @@ let isCreating = $state(false);
|
||||
let newlyCreatedToken = $state<string | null>(null);
|
||||
let tokenCopied = $state(false);
|
||||
|
||||
function formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const diffDays = Math.floor(
|
||||
(Date.now() - new Date(date).getTime()) / 86400000,
|
||||
);
|
||||
if (diffDays === 0) {
|
||||
return "Today";
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return "Yesterday";
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
}
|
||||
if (diffDays < 30) {
|
||||
return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
}
|
||||
return formatDate(date);
|
||||
}
|
||||
|
||||
async function handleCreateToken(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!newTokenName.trim() || isCreating) {
|
||||
@@ -261,9 +235,9 @@ async function handleDelete() {
|
||||
<div>
|
||||
<p class="text-sm font-medium">{token.name}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Created {formatRelativeTime(token.createdAt)}
|
||||
Created {formatRelativeDate(token.createdAt)}
|
||||
{#if token.lastUsedAt}
|
||||
· Last used {formatRelativeTime(token.lastUsedAt)}
|
||||
· Last used {formatRelativeDate(token.lastUsedAt)}
|
||||
{:else}
|
||||
· Never used
|
||||
{/if}
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
Star,
|
||||
Tablet,
|
||||
} from "@lucide/svelte";
|
||||
import { formatRelativeTime } from "@reviq/common";
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { api } from "$lib/api/client";
|
||||
import { ConfirmDialog } from "$lib/components/account";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
@@ -54,31 +54,6 @@ function formatLocation(device: {
|
||||
return parts.length > 0 ? parts.join(", ") : "Unknown location";
|
||||
}
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return "Today";
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return "Yesterday";
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
}
|
||||
if (diffDays < 30) {
|
||||
return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
}
|
||||
return d.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function getDeviceIcon(name: string) {
|
||||
const nameLower = name.toLowerCase();
|
||||
if (
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "$lib/components/ui/card";
|
||||
import { LoadingButton } from "$lib/components/ui/loading-button";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { formatLongDate, formatRole } from "@reviq/common";
|
||||
|
||||
const inviteId = $derived(Number(page.params.inviteId));
|
||||
|
||||
@@ -48,10 +49,8 @@ const acceptMutation = createMutation(() => ({
|
||||
mutationFn: () => api.me.invites.accept({ inviteId }),
|
||||
onSuccess: () => {
|
||||
toast.success("You've joined the organization!");
|
||||
// Invalidate queries
|
||||
queryClient.invalidateQueries({ queryKey: ["me", "invites"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orgs"] });
|
||||
// Redirect to the org dashboard
|
||||
if (inviteQuery.data) {
|
||||
goto(resolve(`/dashboard/${inviteQuery.data.org.slug}` as any));
|
||||
} else {
|
||||
@@ -70,7 +69,6 @@ const declineMutation = createMutation(() => ({
|
||||
mutationFn: () => api.me.invites.decline({ inviteId }),
|
||||
onSuccess: () => {
|
||||
toast.success("Invitation declined");
|
||||
// Invalidate queries
|
||||
queryClient.invalidateQueries({ queryKey: ["me", "invites"] });
|
||||
goto(resolve("/dashboard"));
|
||||
},
|
||||
@@ -81,24 +79,6 @@ const declineMutation = createMutation(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Format role for display
|
||||
*/
|
||||
function formatRole(role: string): string {
|
||||
return role.charAt(0).toUpperCase() + role.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if invite is expiring soon (within 3 days)
|
||||
*/
|
||||
@@ -187,7 +167,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium">Sent on</p>
|
||||
<p class="text-sm text-muted-foreground">{formatDate(new Date(invite.createdAt))}</p>
|
||||
<p class="text-sm text-muted-foreground">{formatLongDate(invite.createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -197,7 +177,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
<div>
|
||||
<p class="text-sm font-medium">Expires on</p>
|
||||
<p class="text-sm {isExpiringSoon(new Date(invite.expiresAt)) ? 'text-warning' : 'text-muted-foreground'}">
|
||||
{formatDate(new Date(invite.expiresAt))}
|
||||
{formatLongDate(invite.expiresAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +187,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
<Alert>
|
||||
<Clock class="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This invitation will expire soon. Accept it before {formatDate(new Date(invite.expiresAt))} to join the organization.
|
||||
This invitation will expire soon. Accept it before {formatLongDate(invite.expiresAt)} to join the organization.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Star,
|
||||
Tablet,
|
||||
} from "@lucide/svelte";
|
||||
import { formatDate, formatRelativeTime } from "@reviq/common";
|
||||
import { createQuery, useQueryClient } from "@tanstack/svelte-query";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
@@ -56,36 +57,6 @@ function formatLocation(session: {
|
||||
return parts.length > 0 ? parts.join(", ") : "Unknown location";
|
||||
}
|
||||
|
||||
function formatDate(date: Date | string): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
return d.toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return "Today";
|
||||
}
|
||||
if (diffDays === 1) {
|
||||
return "Yesterday";
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
}
|
||||
if (diffDays < 30) {
|
||||
return `${Math.floor(diffDays / 7)} weeks ago`;
|
||||
}
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
function parseUserAgent(userAgent: string): {
|
||||
browser: string;
|
||||
os: string;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "$lib/components/ui/table/index.js";
|
||||
import { formatDate } from "$lib/utils/format-date.js";
|
||||
import { formatDate } from "@reviq/common";
|
||||
|
||||
/**
|
||||
* Admin Organizations list page
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "$lib/components/ui/table";
|
||||
import { formatDate } from "$lib/utils/format-date.js";
|
||||
import { formatDate } from "@reviq/common";
|
||||
|
||||
/**
|
||||
* Admin organization details page
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card";
|
||||
import { formatRelativeDate, formatRole } from "@reviq/common";
|
||||
|
||||
/**
|
||||
* Dashboard page - lists all organizations the user is a member of
|
||||
@@ -47,41 +48,6 @@ $effect(() => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Format date to relative or absolute string
|
||||
*/
|
||||
function formatDate(date: Date): string {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return "Today";
|
||||
}
|
||||
if (days === 1) {
|
||||
return "Yesterday";
|
||||
}
|
||||
if (days < 7) {
|
||||
return `${days} days ago`;
|
||||
}
|
||||
if (days < 30) {
|
||||
return `${Math.floor(days / 7)} weeks ago`;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format role for display
|
||||
*/
|
||||
function formatRole(role: string): string {
|
||||
return role.charAt(0).toUpperCase() + role.slice(1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -133,7 +99,7 @@ function formatRole(role: string): string {
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
From {invite.invitedBy} · {formatDate(new Date(invite.createdAt))}
|
||||
From {invite.invitedBy} · {formatRelativeDate(invite.createdAt)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -216,7 +182,7 @@ function formatRole(role: string): string {
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Created {formatDate(new Date(org.createdAt))}
|
||||
Created {formatRelativeDate(org.createdAt)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user