Vincentran commited on
Commit
67c9653
·
1 Parent(s): 978e57b

Upload E-Commerce Product Intelligence Dashboard (frontend + backend)

Browse files
backend/app.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import pandas as pd
4
+ from fastapi import FastAPI, HTTPException, Query
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.responses import HTMLResponse, JSONResponse
7
+ from pathlib import Path
8
+ from huggingface_hub import hf_hub_download
9
+ from typing import Optional
10
+
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ app = FastAPI(title="E-Commerce Product Intelligence Platform")
15
+
16
+ # HF Dataset config
17
+ HF_DATASET_ID = "Vincentran/ecommerce-dataset"
18
+ HF_CSV_PATH = "data/ecommerce_products.csv"
19
+
20
+ # Cache DataFrame
21
+ _data_cache = None
22
+
23
+
24
+ def load_data():
25
+ """Load CSV từ HF Dataset with cache."""
26
+ try:
27
+ if _data_cache is not None:
28
+ logger.info("Using cached DataFrame")
29
+ return _data_cache
30
+
31
+ logger.info(f"Downloading CSV from HF Dataset: {HF_DATASET_ID}/{HF_CSV_PATH}")
32
+ local_csv_path = hf_hub_download(
33
+ repo_id=HF_DATASET_ID,
34
+ filename=HF_CSV_PATH,
35
+ repo_type="dataset"
36
+ )
37
+
38
+ file_size = os.path.getsize(local_csv_path)
39
+ logger.info(f"Loading CSV from: {local_csv_path} (size: {file_size} bytes)")
40
+
41
+ if file_size == 0:
42
+ raise ValueError(f"CSV file is empty: {local_csv_path}")
43
+
44
+ df = pd.read_csv(local_csv_path)
45
+ logger.info(f"Loaded {len(df)} rows, columns: {list(df.columns)}")
46
+
47
+ # Cache DataFrame
48
+ _data_cache = df
49
+ return df
50
+
51
+ except Exception as e:
52
+ logger.error(f"Failed to load data from HF Dataset: {e}")
53
+ raise HTTPException(status_code=500, detail=f"Failed to load data: {str(e)}")
54
+
55
+
56
+ def refresh_cache():
57
+ """Refresh data cache."""
58
+ _data_cache = None
59
+ return load_data()
60
+
61
+
62
+ @app.get("/")
63
+ def root():
64
+ return {"status": "E-Commerce Product Intelligence API is running"}
65
+
66
+
67
+ @app.get("/data")
68
+ def get_data(
69
+ page: int = Query(1, ge=1, description="Page number"),
70
+ limit: int = Query(100, ge=1, le=500, description="Items per page")
71
+ ):
72
+ df = load_data()
73
+ total = len(df)
74
+ start = (page - 1) * limit
75
+ end = start + limit
76
+
77
+ if start >= total:
78
+ raise HTTPException(status_code=404, detail="Page not found")
79
+
80
+ data = df.iloc[start:end].to_dict("records")
81
+ return {
82
+ "data": data,
83
+ "page": page,
84
+ "limit": limit,
85
+ "total": total,
86
+ "total_pages": (total + limit - 1) // limit
87
+ }
88
+
89
+
90
+ @app.get("/stats/categories")
91
+ def stats_categories():
92
+ df = load_data()
93
+ if "category" not in df.columns:
94
+ raise HTTPException(status_code=400, detail="Missing 'category' column")
95
+ return df["category"].value_counts().head(10).to_dict()
96
+
97
+
98
+ @app.get("/stats/brands")
99
+ def stats_brands():
100
+ df = load_data()
101
+ if "brand" not in df.columns:
102
+ raise HTTPException(status_code=400, detail="Missing 'brand' column")
103
+ return df["brand"].value_counts().head(10).to_dict()
104
+
105
+
106
+ @app.get("/stats/price")
107
+ def stats_price():
108
+ df = load_data()
109
+ if "category" not in df.columns or "price" not in df.columns:
110
+ raise HTTPException(status_code=400, detail="Missing 'category' or 'price' column")
111
+ return df.groupby("category")["price"].agg(["mean", "median", "min", "max", "count"]).reset_index().to_dict(
112
+ "records")
113
+
114
+
115
+ @app.get("/stats/rating")
116
+ def stats_rating():
117
+ df = load_data()
118
+ if "category" not in df.columns or "rating" not in df.columns:
119
+ raise HTTPException(status_code=400, detail="Missing 'category' or 'rating' column")
120
+ return df.groupby("category")["rating"].agg(["mean", "median", "min", "max", "count"]).reset_index().to_dict(
121
+ "records")
122
+
123
+
124
+ @app.get("/stats/price-range")
125
+ def stats_price_range():
126
+ """Price distribution by range."""
127
+ df = load_data()
128
+ if "price" not in df.columns:
129
+ raise HTTPException(status_code=400, detail="Missing 'price' column")
130
+
131
+ price_ranges = {
132
+ "Under $50": len(df[df["price"] < 50]),
133
+ "$50 - $100": len(df[(df["price"] >= 50) & (df["price"] < 100)]),
134
+ "$100 - $200": len(df[(df["price"] >= 100) & (df["price"] < 200)]),
135
+ "$200 - $500": len(df[(df["price"] >= 200) & (df["price"] < 500)]),
136
+ "$500+": len(df[df["price"] >= 500])
137
+ }
138
+ return price_ranges
139
+
140
+
141
+ @app.get("/insights")
142
+ def insights():
143
+ df = load_data()
144
+ return JSONResponse(content={
145
+ "total_products": len(df),
146
+ "categories": df["category"].nunique() if "category" in df.columns else 0,
147
+ "brands": df["brand"].nunique() if "brand" in df.columns else 0,
148
+ "avg_price": round(df["price"].mean(), 2) if "price" in df.columns else 0,
149
+ "avg_rating": round(df["rating"].mean(), 2) if "rating" in df.columns else 0,
150
+ "min_price": round(df["price"].min(), 2) if "price" in df.columns else 0,
151
+ "max_price": round(df["price"].max(), 2) if "price" in df.columns else 0,
152
+ })
153
+
154
+
155
+ @app.get("/search")
156
+ def search(
157
+ query: str = Query(..., description="Search query"),
158
+ page: int = Query(1, ge=1, description="Page number"),
159
+ limit: int = Query(100, ge=1, le=500, description="Items per page")
160
+ ):
161
+ df = load_data()
162
+ q = query.lower()
163
+
164
+ # Search only in important columns
165
+ search_cols = ["product_name", "category", "brand", "description"]
166
+ search_cols = [col for col in search_cols if col in df.columns]
167
+
168
+ mask = pd.Series([False] * len(df), index=df.index)
169
+ for col in search_cols:
170
+ try:
171
+ mask |= df[col].str.contains(q, case=False, na=False)
172
+ except:
173
+ pass
174
+
175
+ total = len(df[mask])
176
+ start = (page - 1) * limit
177
+ end = start + limit
178
+
179
+ if start >= total:
180
+ raise HTTPException(status_code=404, detail="No results found")
181
+
182
+ data = df[mask].iloc[start:end].to_dict("records")
183
+ return {
184
+ "data": data,
185
+ "query": query,
186
+ "page": page,
187
+ "limit": limit,
188
+ "total": total,
189
+ "total_pages": (total + limit - 1) // limit
190
+ }
191
+
192
+
193
+ @app.get("/filter")
194
+ def filter_products(
195
+ category: Optional[str] = Query(None, description="Filter by category"),
196
+ min_price: Optional[float] = Query(None, description="Min price"),
197
+ max_price: Optional[float] = Query(None, description="Max price"),
198
+ min_rating: Optional[float] = Query(None, description="Min rating"),
199
+ page: int = Query(1, ge=1, description="Page number"),
200
+ limit: int = Query(100, ge=1, le=500, description="Items per page")
201
+ ):
202
+ df = load_data()
203
+
204
+ # Apply filters
205
+ if category and "category" in df.columns:
206
+ df = df[df["category"] == category]
207
+ if min_price and "price" in df.columns:
208
+ df = df[df["price"] >= min_price]
209
+ if max_price and "price" in df.columns:
210
+ df = df[df["price"] <= max_price]
211
+ if min_rating and "rating" in df.columns:
212
+ df = df[df["rating"] >= min_rating]
213
+
214
+ total = len(df)
215
+ start = (page - 1) * limit
216
+ end = start + limit
217
+
218
+ if start >= total:
219
+ raise HTTPException(status_code=404, detail="No results found")
220
+
221
+ data = df.iloc[start:end].to_dict("records")
222
+ return {
223
+ "data": data,
224
+ "filters": {
225
+ "category": category,
226
+ "min_price": min_price,
227
+ "max_price": max_price,
228
+ "min_rating": min_rating
229
+ },
230
+ "page": page,
231
+ "limit": limit,
232
+ "total": total,
233
+ "total_pages": (total + limit - 1) // limit
234
+ }
235
+
236
+
237
+ @app.get("/recommend")
238
+ def recommend(category: str, limit: int = Query(10, ge=1, le=50, description="Number of recommendations")):
239
+ df = load_data()
240
+ if "category" not in df.columns:
241
+ raise HTTPException(status_code=400, detail="Missing 'category' column")
242
+
243
+ subset = df[df["category"] == category]
244
+ if len(subset) == 0:
245
+ raise HTTPException(status_code=404, detail="No products found in this category")
246
+
247
+ if "rating" in df.columns:
248
+ subset = subset.sort_values("rating", ascending=False)
249
+
250
+ return subset.head(limit).to_dict("records")
251
+
252
+
253
+ @app.post("/refresh-data")
254
+ def refresh_data():
255
+ """Refresh data cache from HF Dataset."""
256
+ try:
257
+ df = refresh_cache()
258
+ return {"status": "Data refreshed successfully", "rows": len(df)}
259
+ except Exception as e:
260
+ raise HTTPException(status_code=500, detail=str(e))
261
+
262
+
263
+ @app.post("/run-scraper")
264
+ def trigger_scraper():
265
+ """Trigger download Kaggle → save CSV → upload to HF."""
266
+ import subprocess
267
+ result = subprocess.run(["python", "backend/scraper.py"], capture_output=True, text=True)
268
+ if result.returncode == 0:
269
+ # Refresh cache after scraper
270
+ refresh_cache()
271
+ return {"status": "Scraper completed successfully", "output": result.stdout}
272
+ else:
273
+ return {"status": "Scraper failed", "error": result.stderr}
274
+
275
+
276
+ # Mount frontend
277
+ frontend_dir = Path("frontend")
278
+ if frontend_dir.exists():
279
+ app.mount("/", StaticFiles(directory=str(frontend_dir), html=True), name="frontend")
280
+ else:
281
+ @app.get("/")
282
+ def frontend_placeholder():
283
+ return HTMLResponse(
284
+ content="<h1>E-Commerce Product Intelligence Dashboard</h1><p>Frontend placeholder.</p>"
285
+ )
backend/main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import uvicorn
3
+
4
+ # Import FastAPI app từ app.py
5
+ import sys
6
+ sys.path.insert(0, '/app')
7
+ from app import app
8
+
9
+ if __name__ == "__main__":
10
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ pandas
3
+ uvicorn
4
+ huggingface_hub
5
+ kaggle
backend/scraper.py CHANGED
@@ -7,8 +7,8 @@ import shutil
7
  # Set Kaggle env vars TRƯỚC khi import Kaggle
8
  token = os.getenv("KAGGLE_API_TOKEN")
9
  if token:
10
- token_value = token.split('_')[1] if '_' in token else token
11
- os.environ['KAGGLE_KEY'] = token_value
12
  os.environ['KAGGLE_USERNAME'] = 'johnsontrann'
13
 
14
  logging.basicConfig(level=logging.INFO)
 
7
  # Set Kaggle env vars TRƯỚC khi import Kaggle
8
  token = os.getenv("KAGGLE_API_TOKEN")
9
  if token:
10
+ # Kaggle API v2:直接使用 access_token
11
+ os.environ['KAGGLE_API_TOKEN'] = token
12
  os.environ['KAGGLE_USERNAME'] = 'johnsontrann'
13
 
14
  logging.basicConfig(level=logging.INFO)
backend/services.py DELETED
@@ -1,22 +0,0 @@
1
- import pandas as pd
2
- from pathlib import Path
3
-
4
- PARQUET_PATH = Path("data/ecommerce_products.parquet")
5
-
6
- def load_data():
7
- """Load parquet."""
8
- if not PARQUET_PATH.exists():
9
- raise FileNotFoundError(f"Parquet not found: {PARQUET_PATH}")
10
- return pd.read_parquet(PARQUET_PATH)
11
-
12
- def get_top_categories(df: pd.DataFrame, n: int = 10):
13
- return df["category"].value_counts().head(n)
14
-
15
- def get_top_brands(df: pd.DataFrame, n: int = 10):
16
- return df["brand"].value_counts().head(n)
17
-
18
- def get_price_stats(df: pd.DataFrame):
19
- return df.groupby("category")["price"].agg(["mean", "median", "min", "max", "count"]).reset_index()
20
-
21
- def get_rating_stats(df: pd.DataFrame):
22
- return df.groupby("category")["rating"].agg(["mean", "median", "min", "max", "count"]).reset_index()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ecommerce-dashboard",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "dev": "vite",
6
+ "build": "vite build",
7
+ "preview": "vite preview"
8
+ },
9
+ "dependencies": {
10
+ "vue": "^3.4.0",
11
+ "echarts": "^5.5.0",
12
+ "vue-echarts": "^6.6.0"
13
+ },
14
+ "devDependencies": {
15
+ "@vitejs/plugin-vue": "^5.0.0",
16
+ "vite": "^5.0.0",
17
+ "vue-router": "^4.2.0",
18
+ "tailwindcss": "^3.4.0",
19
+ "postcss": "^8.4.0",
20
+ "autoprefixer": "^10.4.0"
21
+ }
22
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ }
frontend/src/App.vue ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="container">
3
+ <header>
4
+ <h1>🛍️ E-Commerce Product Intelligence</h1>
5
+ <p>Browse, search, and analyze thousands of products</p>
6
+ </header>
7
+
8
+ <Insights />
9
+ <Charts />
10
+ <SearchFilter @search="handleSearch" />
11
+ <Table
12
+ :products="products"
13
+ :currentPage="currentPage"
14
+ :totalPages="totalPages"
15
+ @page-change="changePage"
16
+ />
17
+ </div>
18
+ </template>
19
+
20
+ <script>
21
+ import { defineComponent } from 'vue'
22
+ import Insights from './components/Insights.vue'
23
+ import Charts from './components/Charts.vue'
24
+ import SearchFilter from './components/SearchFilter.vue'
25
+ import Table from './components/Table.vue'
26
+
27
+ export default defineComponent({
28
+ name: 'App',
29
+ components: { Insights, Charts, SearchFilter, Table },
30
+ data() {
31
+ return {
32
+ products: [],
33
+ currentPage: 1,
34
+ totalPages: 1,
35
+ filters: {}
36
+ }
37
+ },
38
+ methods: {
39
+ async loadProducts() {
40
+ try {
41
+ const params = new URLSearchParams({
42
+ page: this.currentPage,
43
+ limit: 50
44
+ })
45
+ if (this.filters.category) params.append('category', this.filters.category)
46
+ if (this.filters.minPrice) params.append('min_price', this.filters.minPrice)
47
+ if (this.filters.maxPrice) params.append('max_price', this.filters.maxPrice)
48
+ if (this.filters.minRating) params.append('min_rating', this.filters.minRating)
49
+
50
+ const res = await fetch(`/filter?${params}`)
51
+ const data = await res.json()
52
+ this.products = data.data
53
+ this.totalPages = data.total_pages
54
+ } catch (error) {
55
+ console.error('Failed to load products:', error)
56
+ }
57
+ },
58
+ changePage(page) {
59
+ this.currentPage = page
60
+ this.loadProducts()
61
+ },
62
+ handleSearch(filters) {
63
+ this.filters = filters
64
+ this.currentPage = 1
65
+ this.loadProducts()
66
+ }
67
+ },
68
+ mounted() {
69
+ this.loadProducts()
70
+ }
71
+ })
72
+ </script>
73
+
74
+ <style>
75
+ .container {
76
+ max-width: 1400px;
77
+ margin: 0 auto;
78
+ padding: 20px;
79
+ }
80
+
81
+ header {
82
+ background: white;
83
+ padding: 30px;
84
+ text-align: center;
85
+ border-radius: 15px;
86
+ margin-bottom: 30px;
87
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
88
+ }
89
+
90
+ header h1 {
91
+ color: #667eea;
92
+ font-size: 2.5em;
93
+ margin-bottom: 10px;
94
+ }
95
+
96
+ header p {
97
+ color: #666;
98
+ font-size: 1.2em;
99
+ }
100
+ </style>
frontend/src/components/Charts.vue ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="charts-grid">
3
+ <div class="chart-card">
4
+ <h3>📊 Top 10 Categories</h3>
5
+ <VChart :option="categoriesOption" />
6
+ </div>
7
+ <div class="chart-card">
8
+ <h3>🏆 Top 10 Brands</h3>
9
+ <VChart :option="brandsOption" />
10
+ </div>
11
+ <div class="chart-card">
12
+ <h3>💰 Price by Category</h3>
13
+ <VChart :option="priceOption" />
14
+ </div>
15
+ <div class="chart-card">
16
+ <h3>⭐ Rating by Category</h3>
17
+ <VChart :option="ratingOption" />
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script>
23
+ import { defineComponent } from 'vue'
24
+ import VChart from 'vue-echarts'
25
+ import { BarChart, PieChart, LineChart } from 'echarts/charts'
26
+ import { GridComponent, LegendComponent, TitleComponent } from 'echarts/components'
27
+ import { CanvasRenderer } from 'echarts/renderers'
28
+
29
+ VChart.use([
30
+ BarChart,
31
+ PieChart,
32
+ LineChart,
33
+ GridComponent,
34
+ LegendComponent,
35
+ TitleComponent,
36
+ CanvasRenderer
37
+ ])
38
+
39
+ export default defineComponent({
40
+ name: 'Charts',
41
+ components: { VChart },
42
+ data() {
43
+ return {
44
+ categoriesOption: {
45
+ xAxis: { type: 'category', data: [] },
46
+ yAxis: { type: 'value' },
47
+ series: [{ type: 'bar', data: [], itemStyle: { color: '#667eea' } }],
48
+ toolbox: { show: false }
49
+ },
50
+ brandsOption: {
51
+ series: [{
52
+ type: 'pie',
53
+ data: [],
54
+ itemStyle: {
55
+ color: ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#fee140']
56
+ }
57
+ }],
58
+ toolbox: { show: false }
59
+ },
60
+ priceOption: {
61
+ xAxis: { type: 'category', data: [] },
62
+ yAxis: { type: 'value' },
63
+ series: [{ type: 'bar', data: [], itemStyle: { color: '#667eea' } }],
64
+ toolbox: { show: false }
65
+ },
66
+ ratingOption: {
67
+ xAxis: { type: 'category', data: [] },
68
+ yAxis: { type: 'value', min: 0, max: 5 },
69
+ series: [{
70
+ type: 'line',
71
+ data: [],
72
+ smooth: true,
73
+ itemStyle: { color: '#764ba2' },
74
+ areaStyle: { color: 'rgba(118, 75, 162, 0.2)' }
75
+ }],
76
+ toolbox: { show: false }
77
+ }
78
+ }
79
+ },
80
+ methods: {
81
+ async loadCharts() {
82
+ try {
83
+ // Categories
84
+ const catsRes = await fetch('/stats/categories')
85
+ const catsData = await catsRes.json()
86
+ this.categoriesOption.xAxis.data = Object.keys(catsData)
87
+ this.categoriesOption.series[0].data = Object.values(catsData)
88
+
89
+ // Brands
90
+ const brandsRes = await fetch('/stats/brands')
91
+ const brandsData = await brandsRes.json()
92
+ this.brandsOption.series[0].data = Object.keys(brandsData).map((key, i) => ({
93
+ name: key,
94
+ value: brandsData[key]
95
+ }))
96
+
97
+ // Price
98
+ const priceRes = await fetch('/stats/price')
99
+ const priceData = await priceRes.json()
100
+ this.priceOption.xAxis.data = priceData.map(d => d.category)
101
+ this.priceOption.series[0].data = priceData.map(d => d.mean)
102
+
103
+ // Rating
104
+ const ratingRes = await fetch('/stats/rating')
105
+ const ratingData = await ratingRes.json()
106
+ this.ratingOption.xAxis.data = ratingData.map(d => d.category)
107
+ this.ratingOption.series[0].data = ratingData.map(d => d.mean)
108
+ } catch (error) {
109
+ console.error('Failed to load charts:', error)
110
+ }
111
+ }
112
+ },
113
+ mounted() {
114
+ this.loadCharts()
115
+ }
116
+ })
117
+ </script>
118
+
119
+ <style scoped>
120
+ .charts-grid {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
123
+ gap: 30px;
124
+ margin-bottom: 30px;
125
+ }
126
+
127
+ .chart-card {
128
+ background: white;
129
+ padding: 25px;
130
+ border-radius: 15px;
131
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
132
+ }
133
+
134
+ .chart-card h3 {
135
+ color: #667eea;
136
+ margin-bottom: 20px;
137
+ text-align: center;
138
+ }
139
+
140
+ .chart-card div {
141
+ height: 300px;
142
+ }
143
+ </style>
frontend/src/components/Insights.vue ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="insights-grid">
3
+ <div class="insight-card" v-for="(value, key) in insights" :key="key">
4
+ <h3>{{ labels[key] }}</h3>
5
+ <div class="value">{{ formatValue(key, value) }}</div>
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <script>
11
+ export default {
12
+ name: 'Insights',
13
+ data() {
14
+ return {
15
+ insights: {},
16
+ labels: {
17
+ total_products: 'Total Products',
18
+ categories: 'Categories',
19
+ brands: 'Brands',
20
+ avg_price: 'Avg Price',
21
+ avg_rating: 'Avg Rating',
22
+ min_price: 'Min Price',
23
+ max_price: 'Max Price'
24
+ }
25
+ }
26
+ },
27
+ methods: {
28
+ formatValue(key, value) {
29
+ if (key.includes('price')) return `$${value}`
30
+ if (key.includes('rating')) return value
31
+ return value
32
+ },
33
+ async loadInsights() {
34
+ try {
35
+ const res = await fetch('/insights')
36
+ this.insights = await res.json()
37
+ } catch (error) {
38
+ console.error('Failed to load insights:', error)
39
+ }
40
+ }
41
+ },
42
+ mounted() {
43
+ this.loadInsights()
44
+ }
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .insights-grid {
50
+ display: grid;
51
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
52
+ gap: 20px;
53
+ margin-bottom: 30px;
54
+ }
55
+
56
+ .insight-card {
57
+ background: white;
58
+ padding: 25px;
59
+ border-radius: 15px;
60
+ text-align: center;
61
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
62
+ transition: transform 0.3s;
63
+ }
64
+
65
+ .insight-card:hover {
66
+ transform: translateY(-5px);
67
+ }
68
+
69
+ .insight-card h3 {
70
+ color: #666;
71
+ font-size: 0.9em;
72
+ margin-bottom: 10px;
73
+ }
74
+
75
+ .insight-card .value {
76
+ color: #667eea;
77
+ font-size: 2em;
78
+ font-weight: bold;
79
+ }
80
+ </style>
frontend/src/components/SearchFilter.vue ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="search-section">
3
+ <h3>🔍 Search & Filter Products</h3>
4
+ <form class="search-form" @submit.prevent="submit">
5
+ <input
6
+ type="text"
7
+ v-model="query"
8
+ placeholder="Search products..."
9
+ class="search-input"
10
+ />
11
+ <select v-model="category" class="filter-select">
12
+ <option value="">All Categories</option>
13
+ <option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
14
+ </select>
15
+ <input
16
+ type="number"
17
+ v-model="minPrice"
18
+ placeholder="Min Price"
19
+ class="filter-select"
20
+ />
21
+ <input
22
+ type="number"
23
+ v-model="maxPrice"
24
+ placeholder="Max Price"
25
+ class="filter-select"
26
+ />
27
+ <input
28
+ type="number"
29
+ v-model="minRating"
30
+ placeholder="Min Rating"
31
+ step="0.1"
32
+ min="0"
33
+ max="5"
34
+ class="filter-select"
35
+ />
36
+ <button type="submit" class="btn">Search</button>
37
+ </form>
38
+ </div>
39
+ </template>
40
+
41
+ <script>
42
+ export default {
43
+ name: 'SearchFilter',
44
+ data() {
45
+ return {
46
+ query: '',
47
+ category: '',
48
+ minPrice: '',
49
+ maxPrice: '',
50
+ minRating: '',
51
+ categories: []
52
+ }
53
+ },
54
+ methods: {
55
+ submit() {
56
+ this.$emit('search', {
57
+ query: this.query,
58
+ category: this.category,
59
+ minPrice: this.minPrice,
60
+ maxPrice: this.maxPrice,
61
+ minRating: this.minRating
62
+ })
63
+ },
64
+ async loadCategories() {
65
+ try {
66
+ const res = await fetch('/stats/categories')
67
+ const data = await res.json()
68
+ this.categories = Object.keys(data)
69
+ } catch (error) {
70
+ console.error('Failed to load categories:', error)
71
+ }
72
+ }
73
+ },
74
+ mounted() {
75
+ this.loadCategories()
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <style scoped>
81
+ .search-section {
82
+ background: white;
83
+ padding: 25px;
84
+ border-radius: 15px;
85
+ margin-bottom: 30px;
86
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
87
+ }
88
+
89
+ .search-section h3 {
90
+ color: #667eea;
91
+ margin-bottom: 20px;
92
+ }
93
+
94
+ .search-form {
95
+ display: flex;
96
+ gap: 15px;
97
+ flex-wrap: wrap;
98
+ }
99
+
100
+ .search-input {
101
+ flex: 1;
102
+ min-width: 300px;
103
+ padding: 15px;
104
+ border: 2px solid #ddd;
105
+ border-radius: 10px;
106
+ font-size: 1em;
107
+ }
108
+
109
+ .search-input:focus {
110
+ outline: none;
111
+ border-color: #667eea;
112
+ }
113
+
114
+ .filter-select {
115
+ padding: 15px;
116
+ border: 2px solid #ddd;
117
+ border-radius: 10px;
118
+ font-size: 1em;
119
+ background: white;
120
+ }
121
+
122
+ .btn {
123
+ padding: 15px 30px;
124
+ background: #667eea;
125
+ color: white;
126
+ border: none;
127
+ border-radius: 10px;
128
+ font-size: 1em;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .btn:hover {
133
+ background: #764ba2;
134
+ }
135
+
136
+ @media (max-width: 768px) {
137
+ .search-form {
138
+ flex-direction: column;
139
+ }
140
+ .search-input {
141
+ min-width: 100%;
142
+ }
143
+ }
144
+ </style>
frontend/src/components/Table.vue ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="table-section">
3
+ <h3>📦 Products</h3>
4
+ <div class="table-container">
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th>Name</th>
9
+ <th>Category</th>
10
+ <th>Brand</th>
11
+ <th>Price</th>
12
+ <th>Rating</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <tr v-for="product in products" :key="product.product_name">
17
+ <td>{{ product.product_name || '-' }}</td>
18
+ <td>{{ product.category || '-' }}</td>
19
+ <td>{{ product.brand || '-' }}</td>
20
+ <td>${{ product.price || 0 }}</td>
21
+ <td>{{ product.rating || '-' }}</td>
22
+ </tr>
23
+ </tbody>
24
+ </table>
25
+ </div>
26
+ <div class="pagination">
27
+ <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1">Previous</button>
28
+ <span>Page {{ currentPage }} of {{ totalPages }}</span>
29
+ <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">Next</button>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script>
35
+ export default {
36
+ name: 'Table',
37
+ props: {
38
+ products: Array,
39
+ currentPage: Number,
40
+ totalPages: Number
41
+ },
42
+ methods: {
43
+ changePage(page) {
44
+ if (page < 1 || page > this.totalPages) return
45
+ this.$emit('page-change', page)
46
+ }
47
+ }
48
+ }
49
+ </script>
50
+
51
+ <style scoped>
52
+ .table-section {
53
+ background: white;
54
+ padding: 25px;
55
+ border-radius: 15px;
56
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
57
+ }
58
+
59
+ .table-section h3 {
60
+ color: #667eea;
61
+ margin-bottom: 20px;
62
+ }
63
+
64
+ .table-container {
65
+ overflow-x: auto;
66
+ }
67
+
68
+ table {
69
+ width: 100%;
70
+ border-collapse: collapse;
71
+ }
72
+
73
+ th, td {
74
+ padding: 15px;
75
+ text-align: left;
76
+ border-bottom: 1px solid #ddd;
77
+ }
78
+
79
+ th {
80
+ background: #667eea;
81
+ color: white;
82
+ font-weight: bold;
83
+ }
84
+
85
+ tr:hover {
86
+ background: #f5f5f5;
87
+ }
88
+
89
+ .pagination {
90
+ display: flex;
91
+ justify-content: center;
92
+ gap: 10px;
93
+ margin-top: 20px;
94
+ }
95
+
96
+ .pagination button {
97
+ padding: 10px 20px;
98
+ background: #667eea;
99
+ color: white;
100
+ border: none;
101
+ border-radius: 5px;
102
+ cursor: pointer;
103
+ }
104
+
105
+ .pagination button:hover:not(:disabled) {
106
+ background: #764ba2;
107
+ }
108
+
109
+ .pagination button:disabled {
110
+ background: #ddd;
111
+ cursor: not-allowed;
112
+ }
113
+ </style>
frontend/src/index.css ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ * {
6
+ margin: 0;
7
+ padding: 0;
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ body {
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
+ min-height: 100vh;
15
+ }
frontend/src/main.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import './index.css'
4
+
5
+ createApp(App).mount('#app')
frontend/tailwind.config.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ primary: '#667eea',
11
+ secondary: '#764ba2',
12
+ accent: '#f093fb'
13
+ }
14
+ }
15
+ },
16
+ plugins: []
17
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ server: {
7
+ port: 3000,
8
+ proxy: {
9
+ '/data': 'http://localhost:8000',
10
+ '/stats': 'http://localhost:8000',
11
+ '/insights': 'http://localhost:8000',
12
+ '/search': 'http://localhost:8000',
13
+ '/filter': 'http://localhost:8000',
14
+ '/recommend': 'http://localhost:8000'
15
+ }
16
+ }
17
+ })
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- fastapi==0.109.2
2
  uvicorn
3
  pandas
4
  kaggle
 
1
+ fastapi
2
  uvicorn
3
  pandas
4
  kaggle