All these are automatically available through Supabase client:
// Create new session (automatic on page load)
await supabase.from('visitor_sessions').insert({
session_id: currentSessionId,
visitor_id: visitorId,
user_agent: navigator.userAgent,
screen_resolution: "1920x1080",
referrer: "google.com",
landing_page: "/index.html"
});
// Get session data
const { data } = await supabase
.from('visitor_sessions')
.select('*')
.eq('visitor_id', visitorId);// Save chat message
await supabase.from('chat_messages').insert({
session_id: currentSessionId,
visitor_id: visitorId,
message: "What are your skills?",
is_user: true,
ai_response: "I know HTML, CSS, JavaScript..."
});
// Get chat history
const { data } = await supabase
.from('chat_messages')
.select('*')
.eq('visitor_id', visitorId)
.order('timestamp', { ascending: false })
.limit(20);// Submit contact form
await supabase.from('contact_submissions').insert({
visitor_id: visitorId,
name: "John Doe",
email: "john@example.com",
message: "I'd like to work with you!",
session_id: currentSessionId
});
// Get all submissions (admin only)
const { data } = await supabase
.from('contact_submissions')
.select('*')
.order('submitted_at', { ascending: false });// Track any interaction
await supabase.from('user_interactions').insert({
session_id: currentSessionId,
visitor_id: visitorId,
interaction_type: "button_click",
details: { button_name: "Download Resume" }
});Use Case: Build an email list of interested visitors
SQL Setup:
CREATE TABLE newsletter_subscribers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
name TEXT,
source TEXT, -- where they subscribed from
subscribed_at TIMESTAMP DEFAULT NOW(),
status TEXT DEFAULT 'active', -- active, unsubscribed
verification_token TEXT,
verified BOOLEAN DEFAULT false
);
CREATE INDEX idx_newsletter_email ON newsletter_subscribers(email);
CREATE INDEX idx_newsletter_status ON newsletter_subscribers(status);
ALTER TABLE newsletter_subscribers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public subscribe" ON newsletter_subscribers
FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "Allow authenticated read" ON newsletter_subscribers
FOR SELECT TO authenticated USING (true);JavaScript Integration:
// Add to script.js
async function subscribeToNewsletter(email, name = '') {
if (!supabase) return { success: false, error: 'Offline' };
try {
const { error } = await supabase
.from('newsletter_subscribers')
.insert({
email: email,
name: name,
source: 'portfolio_chat',
verification_token: generateToken()
});
if (error) {
if (error.code === '23505') {
return { success: false, error: 'Already subscribed!' };
}
return { success: false, error: error.message };
}
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
function generateToken() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}Use Case: Let visitors react to your projects
SQL Setup:
CREATE TABLE project_reactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
project_name TEXT NOT NULL,
project_url TEXT,
reaction_type TEXT NOT NULL, -- like, love, bookmark, share
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(visitor_id, project_name, reaction_type)
);
CREATE INDEX idx_project_reactions_project ON project_reactions(project_name);
CREATE INDEX idx_project_reactions_type ON project_reactions(reaction_type);
ALTER TABLE project_reactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public insert" ON project_reactions
FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "Allow public read" ON project_reactions
FOR SELECT TO anon USING (true);
-- View to count reactions
CREATE VIEW project_reaction_counts AS
SELECT
project_name,
reaction_type,
COUNT(*) as count
FROM project_reactions
GROUP BY project_name, reaction_type;JavaScript Integration:
async function addReaction(projectName, reactionType) {
if (!supabase) return;
try {
const { error } = await supabase
.from('project_reactions')
.insert({
visitor_id: visitorId,
project_name: projectName,
reaction_type: reactionType
});
if (error && error.code !== '23505') {
console.error('Reaction error:', error);
}
} catch (error) {
console.error('Reaction error:', error);
}
}
async function getReactionCounts(projectName) {
if (!supabase) return null;
const { data } = await supabase
.from('project_reaction_counts')
.select('*')
.eq('project_name', projectName);
return data;
}Use Case: Improve AI responses based on user feedback
SQL Setup:
CREATE TABLE chat_feedback (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
session_id TEXT NOT NULL,
visitor_id TEXT NOT NULL,
message_text TEXT,
ai_response_text TEXT,
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
helpful BOOLEAN,
feedback_comment TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_chat_feedback_rating ON chat_feedback(rating);
CREATE INDEX idx_chat_feedback_helpful ON chat_feedback(helpful);
ALTER TABLE chat_feedback ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public insert" ON chat_feedback
FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "Allow authenticated read" ON chat_feedback
FOR SELECT TO authenticated USING (true);
-- Average rating view
CREATE VIEW ai_response_quality AS
SELECT
AVG(rating) as avg_rating,
COUNT(*) as total_ratings,
SUM(CASE WHEN helpful = true THEN 1 ELSE 0 END) as helpful_count,
SUM(CASE WHEN helpful = false THEN 1 ELSE 0 END) as not_helpful_count
FROM chat_feedback;JavaScript Integration:
async function rateChatResponse(messageText, aiResponse, rating, helpful, comment = '') {
if (!supabase) return;
try {
await supabase.from('chat_feedback').insert({
session_id: currentSessionId,
visitor_id: visitorId,
message_text: messageText,
ai_response_text: aiResponse,
rating: rating,
helpful: helpful,
feedback_comment: comment
});
} catch (error) {
console.error('Feedback error:', error);
}
}Use Case: Let visitors endorse your skills
SQL Setup:
CREATE TABLE skill_endorsements (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
skill_name TEXT NOT NULL,
endorsed_at TIMESTAMP DEFAULT NOW(),
UNIQUE(visitor_id, skill_name)
);
CREATE INDEX idx_skill_endorsements_skill ON skill_endorsements(skill_name);
ALTER TABLE skill_endorsements ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public insert" ON skill_endorsements
FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "Allow public read" ON skill_endorsements
FOR SELECT TO anon USING (true);
-- Count endorsements
CREATE VIEW skill_endorsement_counts AS
SELECT
skill_name,
COUNT(*) as endorsement_count
FROM skill_endorsements
GROUP BY skill_name
ORDER BY endorsement_count DESC;Use Case: Collect testimonials from visitors or clients
SQL Setup:
CREATE TABLE testimonials (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT,
name TEXT NOT NULL,
email TEXT,
company TEXT,
role TEXT,
testimonial TEXT NOT NULL,
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
approved BOOLEAN DEFAULT false,
featured BOOLEAN DEFAULT false,
submitted_at TIMESTAMP DEFAULT NOW(),
approved_at TIMESTAMP
);
CREATE INDEX idx_testimonials_approved ON testimonials(approved);
CREATE INDEX idx_testimonials_featured ON testimonials(featured);
ALTER TABLE testimonials ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public insert" ON testimonials
FOR INSERT TO anon WITH CHECK (true);
CREATE POLICY "Allow public read approved" ON testimonials
FOR SELECT TO anon USING (approved = true);
CREATE POLICY "Allow authenticated full access" ON testimonials
FOR ALL TO authenticated USING (true);SQL Setup:
-- Blog posts
CREATE TABLE blog_posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
excerpt TEXT,
cover_image TEXT,
tags TEXT[],
published BOOLEAN DEFAULT false,
view_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
published_at TIMESTAMP,
updated_at TIMESTAMP
);
-- Blog comments
CREATE TABLE blog_comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
post_id UUID REFERENCES blog_posts(id),
visitor_id TEXT,
name TEXT NOT NULL,
email TEXT,
comment TEXT NOT NULL,
approved BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- Blog reactions
CREATE TABLE blog_reactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
post_id UUID REFERENCES blog_posts(id),
visitor_id TEXT NOT NULL,
reaction_type TEXT, -- like, love, bookmark
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(post_id, visitor_id, reaction_type)
);Use Case: Let people book consultation calls
SQL Setup:
CREATE TABLE availability_slots (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
available BOOLEAN DEFAULT true,
booked_by TEXT,
booked_at TIMESTAMP
);
CREATE TABLE consultation_bookings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
slot_id UUID REFERENCES availability_slots(id),
purpose TEXT,
notes TEXT,
status TEXT DEFAULT 'pending', -- pending, confirmed, completed, cancelled
created_at TIMESTAMP DEFAULT NOW()
);Use Case: Track resume/portfolio downloads
SQL Setup:
CREATE TABLE file_downloads (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
session_id TEXT,
file_name TEXT NOT NULL,
file_type TEXT,
download_source TEXT, -- where they clicked download
downloaded_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_file_downloads_file ON file_downloads(file_name);
-- Download stats
CREATE VIEW download_statistics AS
SELECT
file_name,
COUNT(*) as total_downloads,
COUNT(DISTINCT visitor_id) as unique_downloaders,
MAX(downloaded_at) as last_downloaded
FROM file_downloads
GROUP BY file_name;Use Case: Track where your visitors come from
SQL Setup:
CREATE TABLE referral_sources (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
referrer_url TEXT,
utm_source TEXT,
utm_medium TEXT,
utm_campaign TEXT,
utm_term TEXT,
utm_content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_referral_utm_source ON referral_sources(utm_source);
CREATE VIEW referral_summary AS
SELECT
COALESCE(utm_source, 'direct') as source,
COUNT(*) as visitor_count
FROM referral_sources
GROUP BY utm_source
ORDER BY visitor_count DESC;Use Case: Track what people search for in chat
SQL Setup:
CREATE TABLE search_queries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
visitor_id TEXT NOT NULL,
session_id TEXT,
query TEXT NOT NULL,
results_found BOOLEAN,
clicked_result TEXT,
searched_at TIMESTAMP DEFAULT NOW()
);
CREATE VIEW popular_searches AS
SELECT
query,
COUNT(*) as search_count,
SUM(CASE WHEN results_found THEN 1 ELSE 0 END) as successful_searches
FROM search_queries
GROUP BY query
ORDER BY search_count DESC
LIMIT 20;- β Visitor Sessions (Already implemented)
- β Chat Messages (Already implemented)
- β Contact Form (Already implemented)
- β User Interactions (Already implemented)
- Newsletter Subscription - Build your email list
- Chat Feedback - Improve AI responses
- File Downloads Tracking - Track resume downloads
- Project Reactions - Engagement on projects
- Skills Endorsement - Social proof
- Testimonials - Client feedback
- Blog System - Content marketing
- Availability Calendar - Book consultations
- Referral Tracking - Marketing analytics
Create a simple admin dashboard to view:
<!-- admin-dashboard.html -->
<!DOCTYPE html>
<html>
<head>
<title>Admin Dashboard</title>
</head>
<body>
<h1>Portfolio Analytics</h1>
<div class="stats">
<div>Total Visitors: <span id="totalVisitors"></span></div>
<div>Total Chats: <span id="totalChats"></span></div>
<div>Contact Forms: <span id="totalContacts"></span></div>
</div>
<h2>Popular Questions</h2>
<ul id="popularQuestions"></ul>
<h2>Recent Contacts</h2>
<table id="recentContacts"></table>
</body>
</html>- Never expose service_role key - Only use anon key in frontend
- Enable RLS on all tables - Protect your data
- Validate email inputs - Prevent spam
- Rate limiting - Use Supabase edge functions for this
- Sanitize user inputs - Prevent XSS attacks
Check out: