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
6 changes: 3 additions & 3 deletions app/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1136,9 +1136,9 @@ What's on your mind? Let's dive in. 🌱`,
{/* Mobile Sidebar Overlay */}
{isMobile && isMobileSidebarOpen && (
<div className="fixed inset-0 z-50 bg-background">
<div className="h-full flex flex-col">
<div className="h-full flex flex-col overflow-hidden">
{/* Mobile sidebar header */}
<div className="px-4 py-3.5 border-b-2 border-border flex justify-between items-center">
<div className="px-4 py-3.5 border-b-2 border-border flex justify-between items-center shrink-0">
<button
onClick={() => {
addChat();
Expand All @@ -1157,7 +1157,7 @@ What's on your mind? Let's dive in. 🌱`,
</button>
</div>
{/* Mobile sidebar content */}
<div className="flex-1">
<div className="flex-1 overflow-y-auto">
<Sidebar
conversations={conversations || []}
mutateConversations={mutateConversations}
Expand Down
50 changes: 50 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,53 @@ button:hover {
@apply bg-background text-foreground;
}
}

/* Custom scrollbar styling */
@layer utilities {
/* Scrollbar always reserves space, but only visible on hover */
.scrollbar-hover-only {
/* Firefox - always show thin scrollbar but transparent */
scrollbar-width: thin;
scrollbar-color: transparent transparent;
}

.scrollbar-hover-only::-webkit-scrollbar {
width: 6px; /* Always reserve 6px space */
}

.scrollbar-hover-only::-webkit-scrollbar-track {
background: transparent;
}

.scrollbar-hover-only::-webkit-scrollbar-thumb {
background-color: transparent; /* Hidden by default */
border-radius: 3px;
transition: background-color 0.2s ease;
}

/* Show scrollbar on hover */
.scrollbar-hover-only:hover {
scrollbar-color: rgba(156, 163, 175, 0.5) transparent; /* Firefox */
}

.scrollbar-hover-only:hover::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
}

.scrollbar-hover-only:hover::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.8);
}

/* Dark mode for hover-only scrollbar */
.dark .scrollbar-hover-only:hover {
scrollbar-color: rgba(75, 85, 99, 0.5) transparent; /* Firefox */
}

.dark .scrollbar-hover-only:hover::-webkit-scrollbar-thumb {
background-color: rgba(75, 85, 99, 0.5);
}

.dark .scrollbar-hover-only:hover::-webkit-scrollbar-thumb:hover {
background-color: rgba(75, 85, 99, 0.8);
}
}
4 changes: 2 additions & 2 deletions components/conversationtab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function ConversationTab({
if (loading) {
return (
<div
className={`${departureMono.className} px-2.5 py-2 rounded-xl flex justify-between items-center overflow-hidden w-full`}
className={`${departureMono.className} px-2.5 py-2 rounded-xl flex justify-between items-center overflow-hidden w-full min-h-[48px]`}
>
<div className="flex-1">
<Skeleton height={20} className="mb-1" />
Expand All @@ -95,7 +95,7 @@ export function ConversationTab({
<ContextMenu>
<ContextMenuTrigger asChild>
<div
className={`${departureMono.className} px-2.5 py-2 rounded-xl flex justify-between items-center overflow-hidden cursor-pointer hover:bg-secondary-background transition-colors w-full ${
className={`${departureMono.className} px-2.5 py-2 rounded-xl flex justify-between items-center overflow-hidden cursor-pointer hover:bg-secondary-background transition-colors w-full min-h-[48px] ${
selected ? 'bg-secondary-background' : ''
}`}
onClick={select}
Expand Down
41 changes: 24 additions & 17 deletions components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,29 +198,36 @@ export default function Sidebar({
<div className={`${departureMono.className} h-full w-full`}>
<div className="h-full overflow-hidden bg-background border-r border-border flex flex-col justify-between items-start">
{/* Top section with conversations */}
<div className="self-stretch px-2.5 py-5 flex flex-col justify-start items-start overflow-y-auto flex-1 gap-2">
<div className="self-stretch px-2.5 py-5 flex flex-col justify-start items-start overflow-hidden flex-1 gap-2 min-h-0">
{/* Header */}
<div className="px-2.5 py-1 flex justify-center items-center gap-2.5">
<div className="px-2.5 py-1 flex justify-center items-center gap-2.5 shrink-0">
<div className="text-muted-foreground text-xs font-normal">
Past Chats
</div>
</div>

{/* Conversation list */}
{conversations.length > 0
? conversations.map((cur, i) => (
<ConversationTab
conversation={cur}
select={() => setConversationId(cur.conversationId)}
selected={conversationId === cur.conversationId}
edit={() => editConversation(cur)}
del={() => removeConversation(cur)}
key={i}
/>
))
: Array.from({ length: 5 }).map((_, i) => (
<ConversationTab loading key={i} />
))}
{/* Conversation list */}
<div className="group flex flex-col gap-2 w-full overflow-y-auto min-h-0 scrollbar-hover-only">
<div className="flex flex-col gap-2 w-full">
{conversations.length > 0
? conversations.map((cur, i) => (
<div key={i} className="shrink-0">
<ConversationTab
conversation={cur}
select={() => setConversationId(cur.conversationId)}
selected={conversationId === cur.conversationId}
edit={() => editConversation(cur)}
del={() => removeConversation(cur)}
/>
</div>
))
: Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="shrink-0">
<ConversationTab loading />
</div>
))}
</div>
</div>
</div>

{/* Bottom section with user info */}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@types/retry": "^0.12.5",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.4",
"csv-parse": "^6.1.0",
"dotenv": "^16.4.7",
"encoding": "^0.1.13",
"eslint": "^9.20.1",
Expand Down Expand Up @@ -115,4 +116,3 @@
}
}
}

8 changes: 8 additions & 0 deletions pnpm-lock.yaml

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

142 changes: 142 additions & 0 deletions scripts/export-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const { createClient } = require("@supabase/supabase-js");
const fs = require("fs");
require("dotenv").config({ path: ".env" });

const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
);

async function exportUsers() {
try {
let allUsers = [];
let pageToken = undefined;
const perPage = 10000;
let pageCount = 0;
let totalUsers = 0;
const delayMs = 500; // Delay between requests to avoid rate limits
const csvFilename = "auth_users.csv";
const jsonFilename = "auth_users.json";
let isFirstBatch = true;

console.log("Fetching users from Supabase (1000 per batch)...");
console.log("Adding delay between requests to avoid rate limits...\n");

// Clear existing files or create new ones
fs.writeFileSync(csvFilename, "");
fs.writeFileSync(jsonFilename, "[\n");

do {
pageCount++;

// Build options for listUsers
const options = {
perPage: perPage,
};

// Add page token if we have one (for cursor-based pagination)
if (pageToken) {
options.page = pageToken;
}

const { data, error } = await supabase.auth.admin.listUsers(options);

if (error) throw error;

totalUsers += data.users.length;
console.log(
`Fetched batch ${pageCount}: ${data.users.length} users (Total: ${totalUsers})`,
);

// Convert batch to CSV rows
if (data.users.length > 0) {
const csvData = data.users.map((user) => ({
id: user.id,
email: user.email,
created_at: user.created_at,
last_sign_in_at: user.last_sign_in_at,
email_confirmed_at: user.email_confirmed_at,
phone: user.phone || "",
provider: user.app_metadata?.provider || "",
role: user.role || "",
}));

// Write CSV header only for first batch
if (isFirstBatch) {
const headers = Object.keys(csvData[0]).join(",");
fs.appendFileSync(csvFilename, headers + "\n");
}

// Append CSV rows
const rows = csvData.map((user) =>
Object.values(user)
.map((value) =>
value === null ? "" : `"${String(value).replace(/"/g, '""')}"`,
)
.join(","),
);
fs.appendFileSync(csvFilename, rows.join("\n") + "\n");

// Append to JSON file
const jsonRows = data.users.map((user, index) => {
const jsonStr = JSON.stringify(user, null, 2);
const indentedStr = jsonStr
.split("\n")
.map((line) => " " + line)
.join("\n");
const prefix = isFirstBatch && index === 0 ? "" : ",\n";
return prefix + indentedStr;
});
fs.appendFileSync(jsonFilename, jsonRows.join(""));

allUsers = allUsers.concat(data.users);
isFirstBatch = false;
}

// Update page token for next iteration
pageToken = data.nextPage;

// Add delay to avoid rate limits if we're fetching more pages
if (pageToken) {
console.log(`Waiting ${delayMs}ms before next request...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
} while (pageToken);

// Close JSON array
fs.appendFileSync(jsonFilename, "\n]");

console.log(`\nExport complete! Total users exported: ${totalUsers}`);
console.log(`Files created: ${jsonFilename} and ${csvFilename}`);
} catch (error) {
console.error("Error:", error);
}
}

function convertToCSV(users) {
if (users.length === 0) return "";

// Extract common fields for CSV
const csvData = users.map((user) => ({
id: user.id,
email: user.email,
created_at: user.created_at,
last_sign_in_at: user.last_sign_in_at,
email_confirmed_at: user.email_confirmed_at,
phone: user.phone || "",
provider: user.app_metadata?.provider || "",
role: user.role || "",
}));

const headers = Object.keys(csvData[0]).join(",");
const rows = csvData.map((user) =>
Object.values(user)
.map((value) =>
value === null ? "" : `"${String(value).replace(/"/g, '""')}"`,
)
.join(","),
);
return [headers, ...rows].join("\n");
}

exportUsers();