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
9 changes: 4 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# ── Supabase ──────────────────────────────────────────────
# Your Supabase project URL (found in Project Settings → API)
NEXT_PUBLIC_SUPABASE_URL="https://your-project-ref.supabase.co"
NEXT_PUBLIC_SUPABASE_URL="https://acbqiepwwaaswkmxjlzu.supabase.co"

# Your Supabase anon/public key (found in Project Settings → API)
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFjYnFpZXB3d2Fhc3drbXhqbHp1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODMyMjkyMDEsImV4cCI6MjA5ODgwNTIwMX0.mik5QjphcBc55R_EvcgBZC2c0vEdpnqOVW9ForBuMNg"

# Database connection string — Transaction Pooler (for Prisma queries)
# Found in: Supabase Dashboard → Project Settings → Database → Connection string → Transaction
DATABASE_URL="postgresql://postgres.your-project-ref:password@aws-0-region.pooler.supabase.com:6543/postgres?pgbouncer=true"

DATABASE_URL="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFjYnFpZXB3d2Fhc3drbXhqbHp1Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4MzIyOTIwMSwiZXhwIjoyMDk4ODA1MjAxfQ.cXy2Q3hwp9FRX35vNZphBTTY7xgJaC8K_bl4-H_WV_g"
# Database connection string — Session mode (for Prisma migrations)
# Found in: Supabase Dashboard → Project Settings → Database → Connection string → Session
DIRECT_URL="postgresql://postgres.your-project-ref:password@aws-0-region.pooler.supabase.com:5432/postgres"
DIRECT_URL="DATABASE_URL="postgresql://postgres:<DB_PASSWORD>@aws-<region>.pooler.supabase.com:5432/postgres?sslmode=require""

# ── Groq AI (for AI Insights feature) ────────────────────
# Get your free API key at: https://console.groq.com/keys
Expand Down
143 changes: 143 additions & 0 deletions app/blocks/home/ChatWidget.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
.chatWrapper {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
font-family: sans-serif;
}

/* Floating Button */
.fab {
background: #00ff88;
color: black;
border: none;
padding: 14px;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 15px #00ff88;
animation: glow 2s infinite;
}

/* Chat Box */
.chatBox {
width: 320px;
height: 420px;
background: #111;
color: white;
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);
}

/* Header */
.header {
padding: 10px;
background: #00ff88;
color: black;
display: flex;
justify-content: space-between;
font-weight: 600;
}

/* Chat body */
.body {
flex: 1;
padding: 10px;
overflow-y: auto;
}

/* Messages */
.user {
text-align: right;
margin: 5px 0;
color: white;
}

.bot {
text-align: left;
margin: 5px 0;
color: #00ff88;
text-shadow: 0 0 6px #00ff88;
}

/* Input area */
.inputBox {
display: flex;
}

.inputBox input {
flex: 1;
padding: 8px;
border: none;
outline: none;
}

.inputBox button {
background: #00ff88;
border: none;
color: black;
padding: 8px 12px;
cursor: pointer;
}

/* Suggestions */
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 6px;
}

.suggestions button {
font-size: 10px;
border: none;
padding: 4px 6px;
cursor: pointer;
}

/* Hover effects */
.fab:hover,
.inputBox button:hover,
.suggestions button:hover {
box-shadow: 0 0 10px #00ff88;
transform: scale(1.05);
}

/* Glow animation */
@keyframes glow {
0% { box-shadow: 0 0 5px #00ff88; }
50% { box-shadow: 0 0 20px #00ff88; }
100% { box-shadow: 0 0 5px #00ff88; }
}

.suggestions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px;
}

.suggestionBtn {
background: rgba(0, 255, 136, 0.08);
border: 1px solid rgba(0, 255, 136, 0.4);
color: #00ff88;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}

.suggestionBtn:hover {
background: #00ff88;
color: #0b0f0e;
transform: translateY(-1px);
box-shadow: 0 0 10px rgba(0, 255, 136, 0.4);
}

.suggestionBtn:active {
transform: scale(0.95);
}
135 changes: 135 additions & 0 deletions app/blocks/home/ChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import { useState } from "react";
import styles from "./ChatWidget.module.css";
import { starterPrompts } from "./StarterPrompts";
import { chatbotData } from "./chatbotData";

type Message = {
role: "user" | "assistant";
content: string;
};

export default function ChatWidget() {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);

// 🔁 LOCAL FAQ MATCH FUNCTION
const findLocalAnswer = (text: string) => {
const msg = text.toLowerCase().trim();

const match = chatbotData.find((item) =>
msg.includes(item.question)
);

return match?.answer || null;
};

const sendMessage = async (text: string) => {
if (!text.trim()) return;

const newMessages: Message[] = [
...messages,
{ role: "user", content: text },
];

setMessages(newMessages);
setInput("");
setLoading(true);

// 🔥 STEP 1: LOCAL LOOP CHECK (FAST RESPONSE)
const localAnswer = findLocalAnswer(text);

if (localAnswer) {
setMessages([
...newMessages,
{ role: "assistant", content: localAnswer },
]);
setLoading(false);
return;
}

// 🌐 STEP 2: BACKEND FALLBACK (/api/chat)
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: newMessages }),
});

const data = await res.json();

setMessages([
...newMessages,
{ role: "assistant", content: data.reply || "No response" },
]);
} catch {
setMessages([
...newMessages,
{ role: "assistant", content: "Error connecting to server." },
]);
}

setLoading(false);
};

return (
<div className={styles.chatWrapper}>
{/* Floating button */}
{!open && (
<button className={styles.fab} onClick={() => setOpen(true)}>
💬
</button>
)}

{/* Chat window */}
{open && (
<div className={styles.chatBox}>
<div className={styles.header}>
<span>FlipTrack Assistant</span>
<button onClick={() => setOpen(false)}>✖</button>
</div>

{/* Messages */}
<div className={styles.body}>
{messages.map((m, i) => (
<div
key={i}
className={m.role === "user" ? styles.user : styles.bot}
>
{m.content}
</div>
))}

{loading && <div className={styles.bot}>Typing...</div>}
</div>

{/* Suggested questions */}
<div className={styles.suggestions}>
{chatbotData.map((p) => (
<button
key={p.question}
onClick={() => sendMessage(p.question)}
className={styles.suggestionBtn}
>
{p.question}
</button>
))}
</div>

{/* Input box */}
<div className={styles.inputBox}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about FlipTrack..."
/>
<button onClick={() => sendMessage(input)}>Send</button>
</div>
</div>
)}
</div>
);
}
8 changes: 8 additions & 0 deletions app/blocks/home/StarterPrompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const starterPrompts = [
"What is FlipTrack?",
"Who is FlipTrack designed for?",
"How does inventory management work?",
"How are profits tracked?",
"What AI features are available?",
"How do I get started?"
];
22 changes: 22 additions & 0 deletions app/blocks/home/chatbotData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const chatbotData = [
{
question: "what is fliptrack",
answer:
"FlipTrack is a universal reselling platform that helps you manage inventory, track profits, and analyze your business in real time."
},
{
question: "who is it for",
answer:
"FlipTrack is built for resellers of sneakers, electronics, fashion, collectibles, and any physical goods."
},
{
question: "how does inventory work",
answer:
"You can add items with cost price, selling price, status, and track profit/loss automatically."
},
{
question: "ai features",
answer:
"FlipTrack uses AI to give insights on pricing, selling timing, and profit optimization."
}
];
4 changes: 3 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import type { Route } from "./+types/root";
import { ErrorBoundary as ErrorBoundaryRoot } from "~/components/error-boundary/error-boundary";
Expand Down Expand Up @@ -87,7 +88,8 @@ function RootLayout({ children }: { children: React.ReactNode }) {
</footer>
)}
<Toaster position="top-right" richColors theme="system" />
</>


);
}

Expand Down
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default [
route("/blog/:slug", "routes/blog-post.tsx"),
route("/changelog", "routes/changelog-page.tsx"),
route("/privacy", "routes/privacy-policy.tsx"),
route("/api/chat", "routes/api.chat.ts"),
route("/terms", "routes/terms-of-service.tsx"),

...prefix("/app", [
Expand Down
32 changes: 32 additions & 0 deletions app/routes/api.chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ActionFunctionArgs } from "react-router";

export async function action({ request }: ActionFunctionArgs) { {
try {
const { messages } = await request.json();

const lastMessage = messages?.at(-1)?.content || "";

return new Response(
JSON.stringify({
reply: `FlipTrack AI: ${lastMessage}`,
}),
{
headers: {
"Content-Type": "application/json",
},
}
);
} catch (err) {
return new Response(
JSON.stringify({
reply: "Server error",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
}
Loading
Loading