Skip to content
Open
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
67 changes: 36 additions & 31 deletions components/AnalyticsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
ArrowLeft,
Expand Down Expand Up @@ -34,6 +34,8 @@ type AnalyticsEvent = {
language: string | null;
screen_w: number | null;
screen_h: number | null;
viewport_w: number | null;
viewport_h: number | null;
visitor_id: string | null;
session_id: string | null;
duration_seconds: number | null;
Expand Down Expand Up @@ -84,50 +86,53 @@ const AnalyticsPage: React.FC = () => {
.finally(() => setInitialLoading(false));
}, []);

const fetchAnalytics = async (customDays?: number) => {
const daysToFetch = customDays ?? days;
if (!projectUrl || !dbPassword) {
setError('Please enter your Supabase URL and database password');
return;
}
const fetchAnalytics = useCallback(
async (customDays?: number) => {
const daysToFetch = customDays ?? days;
if (!projectUrl || !dbPassword) {
setError('Please enter your Supabase URL and database password');
return;
}

setLoading(true);
setError(null);

try {
const res = await fetch('/__openbento/analytics/fetch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectUrl, dbPassword, days: daysToFetch }),
});

const data = await res.json();
if (data.ok) {
setEvents(data.events || []);
setIsConfigured(true);
} else {
setError(data.error || 'Failed to fetch analytics');
setLoading(true);
setError(null);

try {
const res = await fetch('/__openbento/analytics/fetch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectUrl, dbPassword, days: daysToFetch }),
});

const data = await res.json();
if (data.ok) {
setEvents(data.events || []);
setIsConfigured(true);
} else {
setError(data.error || 'Failed to fetch analytics');
}
} catch (e) {
setError('Network error: ' + (e as Error).message);
} finally {
setLoading(false);
}
} catch (e) {
setError('Network error: ' + (e as Error).message);
} finally {
setLoading(false);
}
};
},
[days, projectUrl, dbPassword]
);

// Auto-fetch when config is ready
useEffect(() => {
if (!initialLoading && projectUrl && dbPassword && !isConfigured) {
fetchAnalytics();
}
}, [initialLoading, projectUrl, dbPassword]);
}, [initialLoading, projectUrl, dbPassword, isConfigured, fetchAnalytics]);

// Auto-refresh when days change (if already configured)
useEffect(() => {
if (isConfigured && projectUrl && dbPassword) {
fetchAnalytics(days);
}
}, [days]);
}, [days, isConfigured, projectUrl, dbPassword, fetchAnalytics]);

// Compute analytics stats
const stats = useMemo(() => {
Expand Down
28 changes: 20 additions & 8 deletions components/Builder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,23 @@ const Builder: React.FC<BuilderProps> = ({ onBack }) => {
[profile, blocks, setSiteData, autoSave]
);

const applyImportedBento = useCallback(
(newBento: SavedBento) => {
const nextGridVersion = newBento.data.gridVersion ?? GRID_VERSION;
const normalizedBlocks = ensureBlocksHavePositions(newBento.data.blocks);

setActiveBento(newBento);
setGridVersion(nextGridVersion);
setActiveBentoId(newBento.id);
reset({
profile: newBento.data.profile,
blocks: normalizedBlocks,
});
setEditingBlockId(null);
},
[reset]
);

// Note: Block positioning is handled when blocks are created (addBlock function)
// No automatic repositioning to avoid conflicts with user-placed blocks

Expand Down Expand Up @@ -2229,7 +2246,8 @@ const Builder: React.FC<BuilderProps> = ({ onBack }) => {
}
}}
blocks={blocks}
setBlocks={handleSetBlocks}
onBlocksChange={handleSetBlocks}
onBentoImported={applyImportedBento}
/>

{/* 4. AVATAR CROP MODAL */}
Expand Down Expand Up @@ -2269,13 +2287,7 @@ const Builder: React.FC<BuilderProps> = ({ onBack }) => {
<AIGeneratorModal
isOpen={showAIGeneratorModal}
onClose={() => setShowAIGeneratorModal(false)}
onBentoImported={(newBento) => {
// Reload the app with the new bento
setActiveBento(newBento);
handleSetProfile(newBento.data.profile);
handleSetBlocks(newBento.data.blocks);
setGridVersion(newBento.data.gridVersion ?? GRID_VERSION);
}}
onBentoImported={applyImportedBento}
/>

{/* 7. DEPLOY MODAL */}
Expand Down
110 changes: 109 additions & 1 deletion components/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
Database,
Globe,
} from 'lucide-react';
import type { SocialPlatform, UserProfile, BlockData } from '../types';
import type { SocialPlatform, UserProfile, BlockData, SavedBento } from '../types';
import { AVATAR_PLACEHOLDER } from '../constants';
import ImageCropModal from './ImageCropModal';
import {
Expand All @@ -25,6 +25,9 @@ import {
SOCIAL_PLATFORM_OPTIONS,
formatFollowerCount,
} from '../socialPlatforms';
import { importLinktreeToBento } from '../services/linktreeImportService';

const ENABLE_IMPORT = import.meta.env.VITE_ENABLE_IMPORT === 'true';

type SettingsModalProps = {
isOpen: boolean;
Expand All @@ -36,6 +39,7 @@ type SettingsModalProps = {
// For raw JSON editing
blocks?: BlockData[];
onBlocksChange?: (blocks: BlockData[]) => void;
onBentoImported?: (bento: SavedBento) => void;
};

type TabType = 'general' | 'social' | 'seo' | 'analytics' | 'json';
Expand All @@ -49,6 +53,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
onBentoNameChange,
blocks,
onBlocksChange,
onBentoImported,
}) => {
const avatarInputRef = useRef<HTMLInputElement>(null);
const [pendingAvatarSrc, setPendingAvatarSrc] = useState<string | null>(null);
Expand All @@ -62,6 +67,12 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
// JSON editor state
const [jsonText, setJsonText] = useState('');
const [jsonError, setJsonError] = useState<string | null>(null);
const [linktreeInput, setLinktreeInput] = useState('');
const [linktreeImportState, setLinktreeImportState] = useState<{
status: 'loading' | 'success' | 'error';
message: string;
details?: string[];
} | null>(null);

// Supabase Analytics state
const [supabaseProjectUrl, setSupabaseProjectUrl] = useState('');
Expand Down Expand Up @@ -254,6 +265,31 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
}
};

const handleLinktreeImport = async () => {
if (!onBentoImported || !linktreeInput.trim()) return;

setLinktreeImportState({
status: 'loading',
message: 'Importing Linktree content...',
});

try {
const result = await importLinktreeToBento(linktreeInput);
onBentoImported(result.bento);
setLinktreeInput('');
setLinktreeImportState({
status: 'success',
message: `${result.importedCount} link(s) imported into a new Bento.`,
details: result.warnings,
});
} catch (error) {
setLinktreeImportState({
status: 'error',
message: (error as Error).message || 'Linktree import failed.',
});
}
};

const tabs: { id: TabType; label: string; icon: React.ReactNode }[] = [
{ id: 'general', label: 'General', icon: <User size={16} /> },
{ id: 'social', label: 'Social', icon: <Share2 size={16} /> },
Expand Down Expand Up @@ -349,6 +385,78 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
Used as filename when exporting JSON
</p>
</div>

{ENABLE_IMPORT && onBentoImported && (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl space-y-3">
<div>
<label className="block text-[10px] font-bold text-amber-700 uppercase tracking-wider mb-1.5">
Experimental Import
</label>
<p className="text-sm font-semibold text-gray-900">
Import from Linktree
</p>
<p className="text-xs text-amber-800 mt-1">
This feature is experimental. Some links, media, or custom sections
may not be imported correctly.
</p>
</div>

<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
aria-label="Linktree URL or username"
value={linktreeInput}
onChange={(e) => setLinktreeInput(e.target.value)}
placeholder="https://linktr.ee/yourname or yourname"
className="flex-1 bg-white border border-amber-200 rounded-lg px-3 py-2 text-sm text-gray-800 focus:ring-2 focus:ring-amber-400/40 focus:border-amber-400 focus:outline-none transition-all"
/>
<button
type="button"
aria-label="Import from Linktree"
onClick={handleLinktreeImport}
disabled={
!linktreeInput.trim() || linktreeImportState?.status === 'loading'
}
className="px-4 py-2 rounded-lg bg-gray-900 text-white text-sm font-semibold hover:bg-black transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{linktreeImportState?.status === 'loading' ? (
<>
<Loader2 size={16} className="animate-spin" />
Importing...
</>
) : (
<>
<Upload size={16} />
Import Linktree
</>
)}
</button>
</div>

{linktreeImportState && (
<div
className={`rounded-lg border px-3 py-2 ${
linktreeImportState.status === 'error'
? 'bg-red-50 border-red-200 text-red-700'
: linktreeImportState.status === 'success'
? 'bg-green-50 border-green-200 text-green-700'
: 'bg-white border-amber-200 text-amber-800'
}`}
>
<p className="text-sm font-medium">{linktreeImportState.message}</p>
{linktreeImportState.details?.length ? (
<div className="mt-1 space-y-1">
{linktreeImportState.details.map((detail) => (
<p key={detail} className="text-xs">
{detail}
</p>
))}
</div>
) : null}
</div>
)}
</div>
)}
</section>
)}

Expand Down
19 changes: 19 additions & 0 deletions docs/builder/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ OpenBento can be customized through environment variables.
| Variable | Description | Default |
|----------|-------------|---------|
| `VITE_ENABLE_LANDING` | Show landing page before builder | `false` |
| `VITE_ENABLE_IMPORT` | Enable experimental import actions in Settings, including Linktree import | `false` |

## Landing Page

Expand All @@ -26,6 +27,24 @@ VITE_ENABLE_LANDING=true npm run dev
VITE_ENABLE_LANDING=true npm run build
```

## Experimental Import

Import actions in **Settings** are disabled by default.

To enable the experimental import UI:

```bash
VITE_ENABLE_IMPORT=true npm run dev
```

Or for a production build:

```bash
VITE_ENABLE_IMPORT=true npm run build
```

This import flow is experimental. Imported Linktree content may require manual cleanup after import.

## Data Storage

All user data is stored in the browser's localStorage:
Expand Down
Loading