Yup this is it
Browse files- TrendAnalysis.py +1044 -0
- app.py +77 -0
- dataApi.py +245 -0
- datacite.py +294 -0
- dbconnect.py +23 -0
- static/css/dashboard.css +0 -0
- static/css/gra.css +548 -0
- static/css/homestyle.css +1967 -0
- static/css/loginstyle.css +460 -0
- static/css/profilepage.css +654 -0
- static/js/.env +1 -0
- static/js/gra.js +1080 -0
- static/js/home.js +1627 -0
- static/js/login.js +216 -0
- static/js/useractivity.js +251 -0
- templates/contactBoard.html +451 -0
- templates/feedback.html +566 -0
- templates/gra.html +179 -0
- templates/homepage.html +475 -0
- templates/loginpage.html +86 -0
- venuAnalysis.py +837 -0
- venuedata.py +211 -0
TrendAnalysis.py
ADDED
@@ -0,0 +1,1044 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
2 |
+
import pandas as pd
|
3 |
+
import numpy as np
|
4 |
+
import re
|
5 |
+
import json
|
6 |
+
import umap
|
7 |
+
import plotly.io as pio
|
8 |
+
import hdbscan
|
9 |
+
from bertopic import BERTopic
|
10 |
+
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
|
11 |
+
from skopt import gp_minimize
|
12 |
+
from sentence_transformers import SentenceTransformer
|
13 |
+
import torch
|
14 |
+
import random
|
15 |
+
import multiprocessing
|
16 |
+
from sklearn.feature_extraction.text import CountVectorizer
|
17 |
+
from bertopic.vectorizers import ClassTfidfTransformer
|
18 |
+
from bertopic.representation import KeyBERTInspired
|
19 |
+
import optuna
|
20 |
+
import pandas as pd
|
21 |
+
import dash
|
22 |
+
from dash import dcc, html, Input, Output, State
|
23 |
+
import plotly.graph_objects as go
|
24 |
+
import plotly.express as px
|
25 |
+
import numpy as np
|
26 |
+
import dash_bootstrap_components as dbc
|
27 |
+
from fastapi import HTTPException, APIRouter, Request
|
28 |
+
from pydantic import BaseModel
|
29 |
+
import threading
|
30 |
+
import time
|
31 |
+
import webbrowser
|
32 |
+
import asyncio
|
33 |
+
|
34 |
+
|
35 |
+
# Set seed for reproducibility
|
36 |
+
def set_seed(seed=42):
|
37 |
+
random.seed(seed)
|
38 |
+
np.random.seed(seed)
|
39 |
+
torch.manual_seed(seed)
|
40 |
+
torch.cuda.manual_seed_all(seed)
|
41 |
+
torch.backends.cudnn.deterministic = True
|
42 |
+
torch.backends.cudnn.benchmark = False
|
43 |
+
|
44 |
+
|
45 |
+
if __name__ == "__main__":
|
46 |
+
set_seed(42)
|
47 |
+
multiprocessing.freeze_support()
|
48 |
+
|
49 |
+
global TitleName
|
50 |
+
TitleName = "Dashboard"
|
51 |
+
router = APIRouter()
|
52 |
+
|
53 |
+
|
54 |
+
class TrendAnalysisRequest(BaseModel):
|
55 |
+
userId: str
|
56 |
+
topic: str
|
57 |
+
year: str = None
|
58 |
+
page: int = 0
|
59 |
+
|
60 |
+
|
61 |
+
async def fetch_papers_with_pagination(request: Request, userId: str, topic: str, year: str = None, page: int = 0):
|
62 |
+
# Build the query filter
|
63 |
+
query_filter = {"userId": userId, "topic": topic}
|
64 |
+
if year:
|
65 |
+
query_filter["year"] = year
|
66 |
+
|
67 |
+
# Count total matching documents
|
68 |
+
count_pipeline = [
|
69 |
+
{"$match": query_filter},
|
70 |
+
{"$unwind": "$papers"},
|
71 |
+
{"$count": "total_papers"}
|
72 |
+
]
|
73 |
+
collection = request.app.state.collection
|
74 |
+
count_result = await collection.aggregate(count_pipeline).to_list(length=1)
|
75 |
+
total_papers = count_result[0]['total_papers'] if count_result else 0
|
76 |
+
|
77 |
+
print(f"Total papers matching criteria: {total_papers}")
|
78 |
+
|
79 |
+
# If no papers found, return empty result
|
80 |
+
if total_papers == 0:
|
81 |
+
return pd.DataFrame(), 0, 0, 0, 0
|
82 |
+
|
83 |
+
# Define pagination constants
|
84 |
+
papers_per_page = 200
|
85 |
+
min_papers_last_page = 50
|
86 |
+
|
87 |
+
# Calculate basic pagination
|
88 |
+
if total_papers <= papers_per_page:
|
89 |
+
# Simple case: all papers fit in one page
|
90 |
+
total_pages = 1
|
91 |
+
else:
|
92 |
+
# Multiple pages case
|
93 |
+
full_pages = total_papers // papers_per_page
|
94 |
+
remaining = total_papers % papers_per_page
|
95 |
+
|
96 |
+
if remaining >= min_papers_last_page:
|
97 |
+
# If remaining papers meet minimum threshold, create a separate page
|
98 |
+
total_pages = full_pages + 1
|
99 |
+
else:
|
100 |
+
# Otherwise, we'll have exactly 'full_pages' pages
|
101 |
+
# The remaining papers will be added to the last page
|
102 |
+
total_pages = full_pages
|
103 |
+
|
104 |
+
# Ensure page is within valid range
|
105 |
+
if page >= total_pages:
|
106 |
+
return pd.DataFrame(), 0, total_pages, 0, total_papers
|
107 |
+
|
108 |
+
# Calculate skip and limit based on page number
|
109 |
+
if total_pages == 1:
|
110 |
+
# Only one page - return all papers
|
111 |
+
skip = 0
|
112 |
+
limit = total_papers
|
113 |
+
elif page < total_pages - 1:
|
114 |
+
# Regular full page
|
115 |
+
skip = page * papers_per_page
|
116 |
+
limit = papers_per_page
|
117 |
+
else:
|
118 |
+
# Last page - might include remaining papers
|
119 |
+
remaining = total_papers % papers_per_page
|
120 |
+
|
121 |
+
if remaining >= min_papers_last_page or remaining == 0:
|
122 |
+
# Last page with either enough remaining papers or perfectly divided
|
123 |
+
skip = page * papers_per_page
|
124 |
+
limit = remaining if remaining > 0 else papers_per_page
|
125 |
+
else:
|
126 |
+
# Last page with remaining papers that don't meet minimum threshold
|
127 |
+
# We distribute by adding them to the last page
|
128 |
+
skip = (total_pages - 1) * papers_per_page
|
129 |
+
limit = papers_per_page + remaining
|
130 |
+
|
131 |
+
print(f"Pagination: Page {page + 1} of {total_pages}, Skip {skip}, Limit {limit}")
|
132 |
+
|
133 |
+
# MongoDB aggregation pipeline
|
134 |
+
pipeline = [
|
135 |
+
{"$match": query_filter},
|
136 |
+
{"$unwind": "$papers"},
|
137 |
+
{"$replaceRoot": {"newRoot": "$papers"}},
|
138 |
+
{"$project": {
|
139 |
+
"_id": 0,
|
140 |
+
"paperId": 1,
|
141 |
+
"url": 1,
|
142 |
+
"title": 1,
|
143 |
+
"abstract": 1,
|
144 |
+
"citationCount": 1,
|
145 |
+
"influentialCitationCount": 1,
|
146 |
+
"embedding": 1,
|
147 |
+
"publicationDate": 1,
|
148 |
+
"authors": 1
|
149 |
+
}},
|
150 |
+
{"$sort": {"publicationDate": 1}},
|
151 |
+
{"$skip": skip},
|
152 |
+
{"$limit": limit}
|
153 |
+
]
|
154 |
+
|
155 |
+
# Execute the aggregation pipeline
|
156 |
+
cursor = collection.aggregate(pipeline)
|
157 |
+
papers = await cursor.to_list(None)
|
158 |
+
|
159 |
+
papers_count = len(papers)
|
160 |
+
print(f"Papers Retrieved: {papers_count}")
|
161 |
+
|
162 |
+
# Convert to DataFrame
|
163 |
+
df = pd.DataFrame(papers)
|
164 |
+
df = df.sort_values(by="publicationDate")
|
165 |
+
print(df[["paperId", "publicationDate"]].head(10))
|
166 |
+
|
167 |
+
return df, page, total_pages, papers_count, total_papers
|
168 |
+
|
169 |
+
|
170 |
+
# Preprocessing function
|
171 |
+
def clean_text(text):
|
172 |
+
text = str(text).lower()
|
173 |
+
text = re.sub(r"[^a-zA-Z0-9\s]", "", text)
|
174 |
+
return ' '.join([word for word in text.split() if word not in ENGLISH_STOP_WORDS])
|
175 |
+
|
176 |
+
|
177 |
+
# Adaptive clustering and topic modeling
|
178 |
+
def perform_trend_analysis(df):
|
179 |
+
# Convert embeddings
|
180 |
+
def convert_embedding(embedding):
|
181 |
+
return np.array(embedding["vector"], dtype=np.float64) if isinstance(embedding,
|
182 |
+
dict) and "vector" in embedding else None
|
183 |
+
|
184 |
+
df["embedding"] = df["embedding"].apply(convert_embedding)
|
185 |
+
df = df.dropna(subset=["embedding"])
|
186 |
+
|
187 |
+
if df.empty:
|
188 |
+
return df, {}
|
189 |
+
|
190 |
+
df["clean_text"] = (df["abstract"].fillna("")).apply(clean_text)
|
191 |
+
|
192 |
+
def objective(trial):
|
193 |
+
umap_n_components = trial.suggest_int("umap_n_components", 1, 12)
|
194 |
+
umap_min_dist = trial.suggest_float("umap_min_dist", 0.1, 0.8)
|
195 |
+
umap_n_neighbors = trial.suggest_int("umap_n_neighbors", 2, 12)
|
196 |
+
hdbscan_min_cluster_size = trial.suggest_int("hdbscan_min_cluster_size", 2, 10)
|
197 |
+
hdbscan_min_samples = trial.suggest_int("hdbscan_min_samples", 1, 10)
|
198 |
+
hdbscan_cluster_selection_epsilon = trial.suggest_float("hdbscan_cluster_selection_epsilon", 0.2, 0.8)
|
199 |
+
hdbscan_cluster_selection_method = trial.suggest_categorical("hdbscan_cluster_selection_method",
|
200 |
+
["eom", "leaf"])
|
201 |
+
|
202 |
+
reducer_high_dim = umap.UMAP(
|
203 |
+
n_components=umap_n_components,
|
204 |
+
random_state=42,
|
205 |
+
min_dist=umap_min_dist,
|
206 |
+
n_neighbors=umap_n_neighbors,
|
207 |
+
metric="cosine"
|
208 |
+
)
|
209 |
+
reduced_embeddings_high_dim = reducer_high_dim.fit_transform(np.vstack(df["embedding"].values)).astype(
|
210 |
+
np.float64)
|
211 |
+
|
212 |
+
clusterer = hdbscan.HDBSCAN(
|
213 |
+
min_cluster_size=hdbscan_min_cluster_size,
|
214 |
+
min_samples=hdbscan_min_samples,
|
215 |
+
cluster_selection_epsilon=hdbscan_cluster_selection_epsilon,
|
216 |
+
cluster_selection_method=hdbscan_cluster_selection_method,
|
217 |
+
prediction_data=True,
|
218 |
+
core_dist_n_jobs=1
|
219 |
+
)
|
220 |
+
labels = clusterer.fit_predict(reduced_embeddings_high_dim)
|
221 |
+
|
222 |
+
if len(set(labels)) > 1:
|
223 |
+
dbcv_score = hdbscan.validity.validity_index(reduced_embeddings_high_dim, labels)
|
224 |
+
else:
|
225 |
+
dbcv_score = -np.inf
|
226 |
+
|
227 |
+
return dbcv_score
|
228 |
+
|
229 |
+
study = optuna.create_study(
|
230 |
+
direction="maximize",
|
231 |
+
sampler=optuna.samplers.TPESampler(seed=42))
|
232 |
+
study.optimize(objective, n_trials=100)
|
233 |
+
|
234 |
+
best_params = study.best_params
|
235 |
+
umap_model = umap.UMAP(
|
236 |
+
n_components=best_params["umap_n_components"],
|
237 |
+
random_state=42,
|
238 |
+
min_dist=best_params["umap_min_dist"],
|
239 |
+
n_neighbors=best_params["umap_n_neighbors"],
|
240 |
+
metric="cosine"
|
241 |
+
)
|
242 |
+
hdbscan_model = hdbscan.HDBSCAN(
|
243 |
+
min_cluster_size=best_params["hdbscan_min_cluster_size"],
|
244 |
+
min_samples=best_params["hdbscan_min_samples"],
|
245 |
+
cluster_selection_epsilon=best_params["hdbscan_cluster_selection_epsilon"],
|
246 |
+
cluster_selection_method=best_params["hdbscan_cluster_selection_method"],
|
247 |
+
prediction_data=True,
|
248 |
+
core_dist_n_jobs=1
|
249 |
+
)
|
250 |
+
|
251 |
+
vectorizer = CountVectorizer(
|
252 |
+
stop_words=list(ENGLISH_STOP_WORDS),
|
253 |
+
ngram_range=(2, 3)
|
254 |
+
)
|
255 |
+
|
256 |
+
representation_model = KeyBERTInspired()
|
257 |
+
embedding_model = SentenceTransformer("allenai/specter")
|
258 |
+
topic_model = BERTopic(
|
259 |
+
vectorizer_model=vectorizer,
|
260 |
+
umap_model=umap_model,
|
261 |
+
hdbscan_model=hdbscan_model,
|
262 |
+
embedding_model=embedding_model,
|
263 |
+
nr_topics='auto',
|
264 |
+
top_n_words=8,
|
265 |
+
representation_model=representation_model,
|
266 |
+
ctfidf_model=ClassTfidfTransformer(reduce_frequent_words=False, bm25_weighting=True)
|
267 |
+
)
|
268 |
+
|
269 |
+
topics, _ = topic_model.fit_transform(df["clean_text"], np.vstack(df["embedding"].values))
|
270 |
+
df["topic"] = topics
|
271 |
+
topic_labels = {t: " | ".join([word for word, _ in topic_model.get_topic(t)][:8]) for t in set(topics)}
|
272 |
+
|
273 |
+
reduced_embeddings_2d = umap.UMAP(n_components=2, random_state=42).fit_transform(
|
274 |
+
np.vstack(df["embedding"].values)).astype(np.float64)
|
275 |
+
df["x"] = reduced_embeddings_2d[:, 0]
|
276 |
+
df["y"] = reduced_embeddings_2d[:, 1]
|
277 |
+
df["topic_label"] = df["topic"].map(topic_labels)
|
278 |
+
|
279 |
+
return df, topic_labels
|
280 |
+
|
281 |
+
|
282 |
+
def build_dashboard(df, titleNm, topic_year):
|
283 |
+
TitleName = titleNm + "_" + topic_year
|
284 |
+
color_palette = px.colors.qualitative.Vivid
|
285 |
+
unique_topics = sorted(df["topic"].unique())
|
286 |
+
color_map = {topic: color_palette[i % len(color_palette)] for i, topic in enumerate(unique_topics)}
|
287 |
+
|
288 |
+
# Map colors to topics
|
289 |
+
df["color"] = df["topic"].map(color_map)
|
290 |
+
|
291 |
+
# Calculate the number of papers in each cluster
|
292 |
+
cluster_sizes = df.groupby("topic").size().reset_index(name="paper_count")
|
293 |
+
df = df.merge(cluster_sizes, on="topic", how="left")
|
294 |
+
|
295 |
+
# Improved marker scaling with a better range
|
296 |
+
min_size = 50
|
297 |
+
max_size = 140
|
298 |
+
df["marker_size"] = ((df["paper_count"] - df["paper_count"].min()) /
|
299 |
+
(df["paper_count"].max() - df["paper_count"].min())) * (max_size - min_size) + min_size
|
300 |
+
|
301 |
+
# Add log-transformed citation and influence columns
|
302 |
+
df["log_citation"] = np.log1p(df["citationCount"])
|
303 |
+
df["log_influence"] = np.log1p(df["influentialCitationCount"])
|
304 |
+
|
305 |
+
# Bayesian shrinkage for citations and influence
|
306 |
+
global_median_citation = df["log_citation"].median()
|
307 |
+
global_median_influence = df["log_influence"].median()
|
308 |
+
C = 10 # Shrinkage constant
|
309 |
+
|
310 |
+
def bayesian_shrinkage(group, global_median, C):
|
311 |
+
return (group.sum() + C * global_median) / (len(group) + C)
|
312 |
+
|
313 |
+
adjusted_citations = df.groupby("topic")["log_citation"].apply(
|
314 |
+
lambda x: bayesian_shrinkage(x, global_median_citation, C))
|
315 |
+
adjusted_influence = df.groupby("topic")["log_influence"].apply(
|
316 |
+
lambda x: bayesian_shrinkage(x, global_median_influence, C))
|
317 |
+
|
318 |
+
# Merge adjusted metrics back into the dataframe
|
319 |
+
df = df.merge(adjusted_citations.rename("adjusted_citation"), on="topic")
|
320 |
+
df = df.merge(adjusted_influence.rename("adjusted_influence"), on="topic")
|
321 |
+
|
322 |
+
# Calculate global percentiles for thresholds
|
323 |
+
citation_25th = df["adjusted_citation"].quantile(0.25)
|
324 |
+
citation_75th = df["adjusted_citation"].quantile(0.75)
|
325 |
+
influence_25th = df["adjusted_influence"].quantile(0.25)
|
326 |
+
influence_75th = df["adjusted_influence"].quantile(0.75)
|
327 |
+
|
328 |
+
# Enhanced theme classification with more distinct emojis
|
329 |
+
def classify_theme(row):
|
330 |
+
if row["adjusted_citation"] >= citation_75th and row["adjusted_influence"] >= influence_75th:
|
331 |
+
return "🔥 Hot Topic"
|
332 |
+
elif row["adjusted_citation"] <= citation_25th and row["adjusted_influence"] >= influence_75th:
|
333 |
+
return "💎 Gap Opportunity"
|
334 |
+
elif row["adjusted_citation"] >= citation_75th and row["adjusted_influence"] <= influence_25th:
|
335 |
+
return "⚠️ Risky Theme"
|
336 |
+
else:
|
337 |
+
return "🔄 Neutral"
|
338 |
+
|
339 |
+
df["theme"] = df.apply(classify_theme, axis=1)
|
340 |
+
|
341 |
+
# Initialize the Dash app with an improved Bootstrap theme
|
342 |
+
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY]) # DARKLY for a sleek dark theme
|
343 |
+
|
344 |
+
# Create a more visually appealing figure
|
345 |
+
fig = go.Figure()
|
346 |
+
|
347 |
+
# Add subtle grid lines for reference
|
348 |
+
fig.update_xaxes(
|
349 |
+
showgrid=True,
|
350 |
+
gridwidth=0.1,
|
351 |
+
gridcolor='rgba(255, 255, 255, 0.05)',
|
352 |
+
zeroline=False
|
353 |
+
)
|
354 |
+
fig.update_yaxes(
|
355 |
+
showgrid=True,
|
356 |
+
gridwidth=0.1,
|
357 |
+
gridcolor='rgba(255, 255, 255, 0.05)',
|
358 |
+
zeroline=False
|
359 |
+
)
|
360 |
+
|
361 |
+
for topic in unique_topics:
|
362 |
+
topic_data = df[df["topic"] == topic]
|
363 |
+
|
364 |
+
# Get cluster center
|
365 |
+
center_x = topic_data["x"].mean()
|
366 |
+
center_y = topic_data["y"].mean()
|
367 |
+
|
368 |
+
# Get label
|
369 |
+
full_topic_formatted = topic_data['topic_label'].iloc[
|
370 |
+
0] if 'topic_label' in topic_data.columns else f"Cluster {topic}"
|
371 |
+
|
372 |
+
# Add a subtle glow effect with a larger outer circle
|
373 |
+
fig.add_trace(
|
374 |
+
go.Scatter(
|
375 |
+
x=[center_x],
|
376 |
+
y=[center_y],
|
377 |
+
mode="markers",
|
378 |
+
marker=dict(
|
379 |
+
color=color_map[topic],
|
380 |
+
size=topic_data["marker_size"].iloc[0] * 1.2, # Slightly larger for glow effect
|
381 |
+
opacity=0.3,
|
382 |
+
line=dict(width=0),
|
383 |
+
symbol="circle",
|
384 |
+
),
|
385 |
+
showlegend=False,
|
386 |
+
hoverinfo="none",
|
387 |
+
)
|
388 |
+
)
|
389 |
+
|
390 |
+
# Add main cluster circle with enhanced styling
|
391 |
+
fig.add_trace(
|
392 |
+
go.Scatter(
|
393 |
+
x=[center_x],
|
394 |
+
y=[center_y],
|
395 |
+
mode="markers+text",
|
396 |
+
marker=dict(
|
397 |
+
color=color_map[topic],
|
398 |
+
size=topic_data["marker_size"].iloc[0],
|
399 |
+
opacity=0.85,
|
400 |
+
line=dict(width=2, color="white"),
|
401 |
+
symbol="circle",
|
402 |
+
),
|
403 |
+
text=[f"{topic}"],
|
404 |
+
textposition="middle center",
|
405 |
+
textfont=dict(
|
406 |
+
family="Arial Black",
|
407 |
+
size=16,
|
408 |
+
color="white"
|
409 |
+
),
|
410 |
+
name=f"{topic}",
|
411 |
+
hovertemplate=(
|
412 |
+
"<b>Cluster ID:</b> %{text}<br>" +
|
413 |
+
"<b>Name:</b><br>" + full_topic_formatted + "<br>" +
|
414 |
+
"<b>Papers:</b> " + str(topic_data["paper_count"].iloc[0]) + "<br>" +
|
415 |
+
"<b>Popularity:</b> " + (
|
416 |
+
"🔼 High" if topic_data["adjusted_citation"].iloc[0] >= citation_75th else "🔽 Low") +
|
417 |
+
f" (Adjusted Citation: {topic_data['adjusted_citation'].iloc[0]:.2f})<br>" +
|
418 |
+
"<b>Impactfulness:</b> " + (
|
419 |
+
"🔼 High" if topic_data["adjusted_influence"].iloc[0] >= influence_75th else "🔽 Low") +
|
420 |
+
f" (Adjusted Influence: {topic_data['adjusted_influence'].iloc[0]:.2f})<br>" +
|
421 |
+
"<b>Theme:</b> " + topic_data["theme"].iloc[0] +
|
422 |
+
"<extra></extra>"
|
423 |
+
),
|
424 |
+
customdata=[[topic]],
|
425 |
+
)
|
426 |
+
)
|
427 |
+
|
428 |
+
# Add an aesthetic background with gradient
|
429 |
+
fig.update_layout(
|
430 |
+
shapes=[
|
431 |
+
# Improved gradient background
|
432 |
+
dict(
|
433 |
+
type="rect",
|
434 |
+
xref="paper",
|
435 |
+
yref="paper",
|
436 |
+
x0=0,
|
437 |
+
y0=0,
|
438 |
+
x1=1,
|
439 |
+
y1=1,
|
440 |
+
fillcolor="rgba(0, 0, 40, 0.95)",
|
441 |
+
line_width=0,
|
442 |
+
layer="below"
|
443 |
+
),
|
444 |
+
# Add a subtle radial gradient effect
|
445 |
+
dict(
|
446 |
+
type="circle",
|
447 |
+
xref="paper",
|
448 |
+
yref="paper",
|
449 |
+
x0=0.3,
|
450 |
+
y0=0.3,
|
451 |
+
x1=0.7,
|
452 |
+
y1=0.7,
|
453 |
+
fillcolor="rgba(50, 50, 120, 0.2)",
|
454 |
+
line_width=0,
|
455 |
+
layer="below"
|
456 |
+
)
|
457 |
+
],
|
458 |
+
template="plotly_dark",
|
459 |
+
title={
|
460 |
+
'text': f"<b>{TitleName.title()}</b>",
|
461 |
+
'y': 0.97,
|
462 |
+
'x': 0.5,
|
463 |
+
'xanchor': 'center',
|
464 |
+
'yanchor': 'top',
|
465 |
+
'font': dict(
|
466 |
+
family="Arial Black",
|
467 |
+
size=28,
|
468 |
+
color="white",
|
469 |
+
),
|
470 |
+
'xref': 'paper',
|
471 |
+
'yref': 'paper',
|
472 |
+
},
|
473 |
+
margin=dict(l=40, r=40, b=150, t=100),
|
474 |
+
hovermode="closest",
|
475 |
+
xaxis=dict(showticklabels=False),
|
476 |
+
yaxis=dict(showticklabels=False),
|
477 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
478 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
479 |
+
dragmode="pan",
|
480 |
+
legend=dict(
|
481 |
+
orientation="h",
|
482 |
+
yanchor="bottom",
|
483 |
+
y=-0.15,
|
484 |
+
xanchor="center",
|
485 |
+
x=0.5,
|
486 |
+
bgcolor="rgba(30,30,60,0.5)",
|
487 |
+
bordercolor="rgba(255,255,255,0.2)",
|
488 |
+
borderwidth=1
|
489 |
+
),
|
490 |
+
)
|
491 |
+
|
492 |
+
# Add subtle animation options
|
493 |
+
fig.update_layout(
|
494 |
+
updatemenus=[
|
495 |
+
dict(
|
496 |
+
type="buttons",
|
497 |
+
showactive=False,
|
498 |
+
buttons=[
|
499 |
+
dict(
|
500 |
+
label="Reset View",
|
501 |
+
method="relayout",
|
502 |
+
args=[{"xaxis.range": None, "yaxis.range": None}]
|
503 |
+
),
|
504 |
+
],
|
505 |
+
x=0.05,
|
506 |
+
y=0.05,
|
507 |
+
xanchor="left",
|
508 |
+
yanchor="bottom",
|
509 |
+
bgcolor="rgba(50,50,80,0.7)",
|
510 |
+
bordercolor="rgba(255,255,255,0.2)",
|
511 |
+
)
|
512 |
+
]
|
513 |
+
)
|
514 |
+
|
515 |
+
# Enhanced app layout with modern design elements
|
516 |
+
app.layout = dbc.Container(
|
517 |
+
fluid=True,
|
518 |
+
style={
|
519 |
+
"backgroundColor": "#111122",
|
520 |
+
"minHeight": "100vh",
|
521 |
+
"height": "100%",
|
522 |
+
"width": "100%",
|
523 |
+
"backgroundImage": "linear-gradient(135deg, #111122 0%, #15162c 100%)",
|
524 |
+
"padding": "20px"
|
525 |
+
},
|
526 |
+
children=[
|
527 |
+
dbc.Row([
|
528 |
+
dbc.Col(html.H1(
|
529 |
+
"Trend Analysis Dashboard ",
|
530 |
+
style={
|
531 |
+
"textAlign": "center",
|
532 |
+
"color": "white",
|
533 |
+
"marginBottom": "5px",
|
534 |
+
"fontFamily": "Arial Black",
|
535 |
+
"textShadow": "2px 2px 8px rgba(0,0,0,0.7)",
|
536 |
+
"letterSpacing": "2px",
|
537 |
+
"fontSize": "42px",
|
538 |
+
"background": "linear-gradient(135deg, #790091 0%, #565cd5 100%)",
|
539 |
+
"WebkitBackgroundClip": "text",
|
540 |
+
"WebkitTextFillColor": "transparent",
|
541 |
+
"paddingTop": "10px"
|
542 |
+
}
|
543 |
+
), width=10),
|
544 |
+
|
545 |
+
dbc.Col([
|
546 |
+
html.Button(
|
547 |
+
[
|
548 |
+
html.I(className="fas fa-download mr-2"),
|
549 |
+
" Save Dashboard"
|
550 |
+
],
|
551 |
+
id="download-button",
|
552 |
+
className="btn btn-outline-light",
|
553 |
+
style={
|
554 |
+
"marginTop": "10px",
|
555 |
+
"backgroundColor": "rgba(80, 80, 150, 0.4)",
|
556 |
+
"border": "1px solid rgba(100, 100, 200, 0.5)",
|
557 |
+
"borderRadius": "8px",
|
558 |
+
"padding": "8px 15px",
|
559 |
+
"boxShadow": "0px 4px 8px rgba(0, 0, 0, 0.3)",
|
560 |
+
"transition": "all 0.3s ease",
|
561 |
+
"fontSize": "14px",
|
562 |
+
"fontWeight": "bold"
|
563 |
+
}
|
564 |
+
),
|
565 |
+
# Add the download component
|
566 |
+
dcc.Download(id="download-dashboard")
|
567 |
+
], width=2),
|
568 |
+
|
569 |
+
dbc.Col(html.P(
|
570 |
+
"Interactive visualization of research topics and their relationships",
|
571 |
+
style={
|
572 |
+
"textAlign": "center",
|
573 |
+
"color": "#aaddff",
|
574 |
+
"marginBottom": "15px",
|
575 |
+
"fontStyle": "italic",
|
576 |
+
"fontSize": "16px",
|
577 |
+
"fontWeight": "300",
|
578 |
+
"letterSpacing": "0.5px",
|
579 |
+
"textShadow": "1px 1px 3px rgba(0,0,0,0.5)",
|
580 |
+
}
|
581 |
+
), width=12),
|
582 |
+
]),
|
583 |
+
|
584 |
+
dbc.Row([
|
585 |
+
dbc.Col(
|
586 |
+
dbc.Card(
|
587 |
+
dbc.CardBody([
|
588 |
+
dcc.Graph(
|
589 |
+
id="cluster-graph",
|
590 |
+
figure=fig,
|
591 |
+
config={
|
592 |
+
"scrollZoom": True,
|
593 |
+
"displayModeBar": True,
|
594 |
+
"modeBarButtonsToRemove": ["select2d", "lasso2d"]
|
595 |
+
}, style={"height": "80vh", "min-height": "800px"}
|
596 |
+
)
|
597 |
+
], style={"height": "80vh", "min-height": "800px"}),
|
598 |
+
style={
|
599 |
+
"backgroundColor": "rgba(20, 20, 40, 0.7)",
|
600 |
+
"borderRadius": "15px",
|
601 |
+
"boxShadow": "0px 10px 30px rgba(0, 0, 0, 0.5)",
|
602 |
+
"border": "1px solid rgba(100, 100, 200, 0.3)",
|
603 |
+
"height": "80vh",
|
604 |
+
"min-height": "800px" # Ensure minimum height
|
605 |
+
}
|
606 |
+
),
|
607 |
+
width=9
|
608 |
+
),
|
609 |
+
|
610 |
+
dbc.Col(
|
611 |
+
dbc.Card(
|
612 |
+
dbc.CardBody([
|
613 |
+
html.H3("Paper List", style={
|
614 |
+
"textAlign": "center",
|
615 |
+
"marginBottom": "15px",
|
616 |
+
"color": "#ffffff",
|
617 |
+
"fontFamily": "Arial",
|
618 |
+
"fontWeight": "bold",
|
619 |
+
"textShadow": "1px 1px 3px rgba(0,0,0,0.3)"
|
620 |
+
}),
|
621 |
+
html.Hr(style={"borderColor": "rgba(100, 100, 200, 0.3)", "margin": "10px 0 20px 0"}),
|
622 |
+
html.Div(
|
623 |
+
id="paper-list",
|
624 |
+
style={
|
625 |
+
"overflowY": "auto",
|
626 |
+
"height": "700px",
|
627 |
+
"padding": "5px"
|
628 |
+
},
|
629 |
+
children=html.Div([
|
630 |
+
html.Div(
|
631 |
+
html.I(className="fas fa-mouse-pointer", style={"marginRight": "10px"}),
|
632 |
+
style={"textAlign": "center", "fontSize": "24px", "marginBottom": "10px",
|
633 |
+
"color": "#7f8fa6"}
|
634 |
+
),
|
635 |
+
html.P("Click on a cluster to view its papers",
|
636 |
+
style={"textAlign": "center", "color": "#7f8fa6"})
|
637 |
+
])
|
638 |
+
),
|
639 |
+
],
|
640 |
+
style={
|
641 |
+
"backgroundColor": "rgba(30, 30, 50, 0.8)",
|
642 |
+
"borderRadius": "15px",
|
643 |
+
"padding": "20px",
|
644 |
+
"height": "100%"
|
645 |
+
}),
|
646 |
+
style={
|
647 |
+
"height": "800px",
|
648 |
+
"boxShadow": "0px 10px 30px rgba(0, 0, 0, 0.5)",
|
649 |
+
"border": "1px solid rgba(100, 100, 200, 0.3)",
|
650 |
+
"borderRadius": "15px"
|
651 |
+
}
|
652 |
+
),
|
653 |
+
width=3
|
654 |
+
),
|
655 |
+
], style={"marginTop": "20px"}),
|
656 |
+
|
657 |
+
# Add a footer with theme legend
|
658 |
+
dbc.Row([
|
659 |
+
dbc.Col(
|
660 |
+
dbc.Card(
|
661 |
+
dbc.CardBody([
|
662 |
+
html.H5("Theme Legend", style={"textAlign": "center", "marginBottom": "15px"}),
|
663 |
+
dbc.Row([
|
664 |
+
dbc.Col(html.Div([
|
665 |
+
html.Span("🔥", style={"fontSize": "20px", "marginRight": "10px"}),
|
666 |
+
"Hot Topic: High citations & high influence"
|
667 |
+
]), width=3),
|
668 |
+
dbc.Col(html.Div([
|
669 |
+
html.Span("💎", style={"fontSize": "20px", "marginRight": "10px"}),
|
670 |
+
"Gap Opportunity: Low citations but high influence"
|
671 |
+
]), width=3),
|
672 |
+
dbc.Col(html.Div([
|
673 |
+
html.Span("⚠️", style={"fontSize": "20px", "marginRight": "10px"}),
|
674 |
+
"Risky Theme: High citations but low influence"
|
675 |
+
]), width=3),
|
676 |
+
dbc.Col(html.Div([
|
677 |
+
html.Span("🔄", style={"fontSize": "20px", "marginRight": "10px"}),
|
678 |
+
"Neutral: Average citations and influence"
|
679 |
+
]), width=3),
|
680 |
+
])
|
681 |
+
]),
|
682 |
+
style={
|
683 |
+
"backgroundColor": "rgba(30, 30, 50, 0.8)",
|
684 |
+
"borderRadius": "15px",
|
685 |
+
"marginTop": "20px",
|
686 |
+
"boxShadow": "0px 5px 15px rgba(0, 0, 0, 0.3)",
|
687 |
+
"border": "1px solid rgba(100, 100, 200, 0.3)"
|
688 |
+
}
|
689 |
+
),
|
690 |
+
width=12
|
691 |
+
),
|
692 |
+
|
693 |
+
]),
|
694 |
+
|
695 |
+
dcc.Store(id="stored-figure", data=fig)
|
696 |
+
]
|
697 |
+
|
698 |
+
)
|
699 |
+
|
700 |
+
@app.callback(
|
701 |
+
Output("download-dashboard", "data"),
|
702 |
+
Input("download-button", "n_clicks"),
|
703 |
+
State("cluster-graph", "figure"),
|
704 |
+
prevent_initial_call=True
|
705 |
+
)
|
706 |
+
def download_dashboard(n_clicks, figure):
|
707 |
+
if n_clicks is None:
|
708 |
+
return None
|
709 |
+
|
710 |
+
# Save the figure as HTML with full plotly.js included
|
711 |
+
dashboard_html = pio.to_html(
|
712 |
+
figure,
|
713 |
+
full_html=True,
|
714 |
+
include_plotlyjs='cdn',
|
715 |
+
config={'responsive': True}
|
716 |
+
)
|
717 |
+
|
718 |
+
# Return the dashboard as an HTML file
|
719 |
+
return dict(
|
720 |
+
content=dashboard_html,
|
721 |
+
filename="research_dashboard.html",
|
722 |
+
type="text/html",
|
723 |
+
)
|
724 |
+
|
725 |
+
# Enhanced callback to update paper list with better styling
|
726 |
+
# Enhanced callback to update paper list with better styling
|
727 |
+
@app.callback(
|
728 |
+
Output("paper-list", "children"),
|
729 |
+
[Input("cluster-graph", "clickData")]
|
730 |
+
)
|
731 |
+
def update_paper_list(clickData):
|
732 |
+
if clickData is None:
|
733 |
+
return html.Div([
|
734 |
+
html.Div(
|
735 |
+
html.I(className="fas fa-mouse-pointer", style={"marginRight": "10px"}),
|
736 |
+
style={"textAlign": "center", "fontSize": "24px", "marginBottom": "10px", "color": "#7f8fa6"}
|
737 |
+
),
|
738 |
+
html.P("Click on a cluster to view its papers",
|
739 |
+
style={"textAlign": "center", "color": "#7f8fa6"})
|
740 |
+
])
|
741 |
+
|
742 |
+
# Extract the clicked cluster ID
|
743 |
+
try:
|
744 |
+
clicked_topic = clickData["points"][0]["customdata"][0]
|
745 |
+
|
746 |
+
# Get the color for this topic for styling consistency
|
747 |
+
topic_color = color_map[clicked_topic]
|
748 |
+
|
749 |
+
# Get the theme for this topic
|
750 |
+
topic_theme = df[df["topic"] == clicked_topic]["theme"].iloc[0]
|
751 |
+
|
752 |
+
except (KeyError, IndexError):
|
753 |
+
return html.Div("Error retrieving cluster data.", style={"textAlign": "center", "marginTop": "20px"})
|
754 |
+
|
755 |
+
# Filter papers in the clicked cluster - UPDATED to include titles AND urls
|
756 |
+
papers_in_cluster = df[df["topic"] == clicked_topic][["title", "url", "paperId"]]
|
757 |
+
|
758 |
+
if papers_in_cluster.empty:
|
759 |
+
return html.Div(f"No papers found for Cluster {clicked_topic}.",
|
760 |
+
style={"textAlign": "center", "marginTop": "20px"})
|
761 |
+
|
762 |
+
# Get topic label
|
763 |
+
topic_label = df[df["topic"] == clicked_topic]['topic_label'].iloc[
|
764 |
+
0] if 'topic_label' in df.columns else f"Cluster {clicked_topic}"
|
765 |
+
|
766 |
+
# Create an enhanced styled list of paper titles - UPDATED to make clickable
|
767 |
+
paper_list = []
|
768 |
+
for i, (_, paper) in enumerate(papers_in_cluster.iterrows()):
|
769 |
+
paper_url = paper["url"]
|
770 |
+
paper_title = paper["title"]
|
771 |
+
|
772 |
+
paper_list.append(
|
773 |
+
dbc.Card(
|
774 |
+
dbc.CardBody([
|
775 |
+
html.A(
|
776 |
+
html.H6(
|
777 |
+
f"{i + 1}. {paper_title}",
|
778 |
+
className="card-title",
|
779 |
+
style={
|
780 |
+
"fontSize": "14px",
|
781 |
+
"margin": "5px 0",
|
782 |
+
"fontWeight": "normal",
|
783 |
+
"lineHeight": "1.4",
|
784 |
+
"color": "#aaccff", # Blue color to indicate clickable link
|
785 |
+
"cursor": "pointer"
|
786 |
+
}
|
787 |
+
),
|
788 |
+
href=paper_url,
|
789 |
+
target="_blank", # Open in new tab
|
790 |
+
style={"textDecoration": "none"}
|
791 |
+
),
|
792 |
+
], style={"padding": "12px"}),
|
793 |
+
style={
|
794 |
+
"marginBottom": "10px",
|
795 |
+
"backgroundColor": "rgba(40, 45, 60, 0.8)",
|
796 |
+
"borderRadius": "8px",
|
797 |
+
"borderLeft": f"4px solid {topic_color}",
|
798 |
+
"boxShadow": "0px 3px 8px rgba(0, 0, 0, 0.2)",
|
799 |
+
"transition": "transform 0.2s",
|
800 |
+
":hover": {
|
801 |
+
"transform": "translateY(-2px)",
|
802 |
+
"boxShadow": "0px 5px 10px rgba(0, 0, 0, 0.3)"
|
803 |
+
}
|
804 |
+
},
|
805 |
+
className="paper-card"
|
806 |
+
)
|
807 |
+
)
|
808 |
+
|
809 |
+
return html.Div([
|
810 |
+
html.Div([
|
811 |
+
html.H4(
|
812 |
+
f"Cluster {clicked_topic}",
|
813 |
+
style={
|
814 |
+
"textAlign": "center",
|
815 |
+
"marginBottom": "5px",
|
816 |
+
"color": topic_color,
|
817 |
+
"fontWeight": "bold"
|
818 |
+
}
|
819 |
+
),
|
820 |
+
html.H5(
|
821 |
+
topic_label,
|
822 |
+
style={
|
823 |
+
"textAlign": "center",
|
824 |
+
"marginBottom": "5px",
|
825 |
+
"color": "#aaaacc",
|
826 |
+
"fontStyle": "italic",
|
827 |
+
"fontWeight": "normal"
|
828 |
+
}
|
829 |
+
),
|
830 |
+
html.Div(
|
831 |
+
topic_theme,
|
832 |
+
style={
|
833 |
+
"textAlign": "center",
|
834 |
+
"marginBottom": "15px",
|
835 |
+
"fontSize": "16px",
|
836 |
+
"fontWeight": "bold"
|
837 |
+
}
|
838 |
+
),
|
839 |
+
html.Hr(style={"borderColor": "rgba(100, 100, 200, 0.3)", "margin": "10px 0 20px 0"}),
|
840 |
+
html.H5(
|
841 |
+
f"Papers ({len(papers_in_cluster)})",
|
842 |
+
style={
|
843 |
+
"textAlign": "left",
|
844 |
+
"marginBottom": "15px",
|
845 |
+
"color": "#ffffff",
|
846 |
+
"fontWeight": "bold"
|
847 |
+
}
|
848 |
+
),
|
849 |
+
]),
|
850 |
+
html.Div(
|
851 |
+
paper_list,
|
852 |
+
style={"paddingRight": "10px"},
|
853 |
+
)
|
854 |
+
])
|
855 |
+
|
856 |
+
# Add custom CSS for hover effects
|
857 |
+
app.index_string = '''
|
858 |
+
<!DOCTYPE html>
|
859 |
+
<html>
|
860 |
+
<head>
|
861 |
+
{%metas%}
|
862 |
+
<title>Trend Analysis Clusters Dashboard</title>
|
863 |
+
{%favicon%}
|
864 |
+
{%css%}
|
865 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
866 |
+
<style>
|
867 |
+
.paper-card:hover {
|
868 |
+
transform: translateY(-2px);
|
869 |
+
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
870 |
+
background-color: rgba(50, 55, 70, 0.8) !important;
|
871 |
+
}
|
872 |
+
a h6:hover {
|
873 |
+
color: #ffffff !important;
|
874 |
+
text-decoration: underline;
|
875 |
+
}
|
876 |
+
/* Add subtle scroll bar styling */
|
877 |
+
::-webkit-scrollbar {
|
878 |
+
width: 8px;
|
879 |
+
}
|
880 |
+
::-webkit-scrollbar-track {
|
881 |
+
background: rgba(30, 30, 50, 0.3);
|
882 |
+
border-radius: 10px;
|
883 |
+
}
|
884 |
+
::-webkit-scrollbar-thumb {
|
885 |
+
background: rgba(100, 100, 200, 0.5);
|
886 |
+
border-radius: 10px;
|
887 |
+
}
|
888 |
+
::-webkit-scrollbar-thumb:hover {
|
889 |
+
background: rgba(120, 120, 220, 0.7);
|
890 |
+
}
|
891 |
+
</style>
|
892 |
+
</head>
|
893 |
+
<body>
|
894 |
+
{%app_entry%}
|
895 |
+
<footer>
|
896 |
+
{%config%}
|
897 |
+
{%scripts%}
|
898 |
+
{%renderer%}
|
899 |
+
</footer>
|
900 |
+
</body>
|
901 |
+
</html>
|
902 |
+
'''
|
903 |
+
return app
|
904 |
+
|
905 |
+
|
906 |
+
# Global variables to track Dash app state
|
907 |
+
dash_thread = None
|
908 |
+
dash_app = None
|
909 |
+
DASH_PORT = 7050
|
910 |
+
|
911 |
+
|
912 |
+
# Simplified shutdown function that doesn't rely on request or psutil connections
|
913 |
+
def shutdown_dash_app():
|
914 |
+
global dash_thread, dash_app
|
915 |
+
|
916 |
+
if dash_app is not None:
|
917 |
+
try:
|
918 |
+
print("Shutting down previous Dash app...")
|
919 |
+
|
920 |
+
# If we have a Dash app with a server
|
921 |
+
if hasattr(dash_app, 'server'):
|
922 |
+
# Set a shutdown flag
|
923 |
+
dash_app._shutdown = True
|
924 |
+
|
925 |
+
# Force the thread to terminate
|
926 |
+
if dash_thread and dash_thread.is_alive():
|
927 |
+
import ctypes
|
928 |
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
929 |
+
ctypes.c_long(dash_thread.ident),
|
930 |
+
ctypes.py_object(SystemExit)
|
931 |
+
)
|
932 |
+
dash_thread.join(timeout=2)
|
933 |
+
|
934 |
+
# Try to find and kill the process using the port
|
935 |
+
try:
|
936 |
+
import psutil
|
937 |
+
import os
|
938 |
+
import signal
|
939 |
+
|
940 |
+
for proc in psutil.process_iter(['pid']):
|
941 |
+
try:
|
942 |
+
for conn in proc.connections(kind='inet'):
|
943 |
+
if conn.laddr.port == DASH_PORT:
|
944 |
+
print(f"Killing process {proc.pid} using port {DASH_PORT}")
|
945 |
+
os.kill(proc.pid, signal.SIGTERM)
|
946 |
+
except:
|
947 |
+
pass
|
948 |
+
except:
|
949 |
+
print("Could not find process using port")
|
950 |
+
|
951 |
+
# Clear references
|
952 |
+
dash_app = None
|
953 |
+
print("Previous Dash app successfully shut down")
|
954 |
+
return True
|
955 |
+
|
956 |
+
except Exception as e:
|
957 |
+
print(f"Error shutting down Dash app: {e}")
|
958 |
+
# Even if there were errors, reset the state
|
959 |
+
dash_app = None
|
960 |
+
return True
|
961 |
+
|
962 |
+
return True # No app to shut down
|
963 |
+
|
964 |
+
|
965 |
+
# Updated function to run Dash with error handling
|
966 |
+
def run_dash(df, titleNm, Topic_year):
|
967 |
+
global dash_app
|
968 |
+
|
969 |
+
try:
|
970 |
+
# Build the dashboard
|
971 |
+
dash_app = build_dashboard(df, titleNm, Topic_year)
|
972 |
+
|
973 |
+
# Run the server
|
974 |
+
dash_app.run_server(debug=False, port=DASH_PORT, use_reloader=False)
|
975 |
+
except Exception as e:
|
976 |
+
print(f"Error running Dash app: {e}")
|
977 |
+
dash_app = None
|
978 |
+
|
979 |
+
|
980 |
+
# Update your endpoint - removed request parameter from shutdown_dash_app
|
981 |
+
@router.post("/analyze-trends/")
|
982 |
+
async def analyze_trends(request: Request, data_request: TrendAnalysisRequest):
|
983 |
+
global dash_thread
|
984 |
+
TitleName = data_request.topic
|
985 |
+
Topic_year = data_request.year
|
986 |
+
# First, ensure any existing dashboard is properly shut down
|
987 |
+
shutdown_dash_app()
|
988 |
+
|
989 |
+
# Short delay to ensure port is freed
|
990 |
+
import time
|
991 |
+
time.sleep(1)
|
992 |
+
|
993 |
+
# Fetch and process data
|
994 |
+
df, current_page, total_pages, papers_count, total_papers = await fetch_papers_with_pagination(
|
995 |
+
request, data_request.userId, data_request.topic, data_request.year, data_request.page
|
996 |
+
)
|
997 |
+
|
998 |
+
if df.empty and total_papers > 0:
|
999 |
+
raise HTTPException(
|
1000 |
+
status_code=404,
|
1001 |
+
detail=f"No papers found for page {data_request.page + 1}. Valid pages are 1 to {total_pages}."
|
1002 |
+
)
|
1003 |
+
elif df.empty:
|
1004 |
+
raise HTTPException(
|
1005 |
+
status_code=404,
|
1006 |
+
detail=f"No papers found for userId '{data_request.userId}', topic '{data_request.topic}'" +
|
1007 |
+
(f", and year '{data_request.year}'" if data_request.year else "")
|
1008 |
+
)
|
1009 |
+
|
1010 |
+
# Perform the trend analysis
|
1011 |
+
df, topic_labels = perform_trend_analysis(df)
|
1012 |
+
|
1013 |
+
if df.empty:
|
1014 |
+
raise HTTPException(status_code=500, detail="Failed to process embeddings for trend analysis")
|
1015 |
+
|
1016 |
+
# Create cluster statistics
|
1017 |
+
cluster_sizes = df.groupby("topic").size().to_dict()
|
1018 |
+
|
1019 |
+
# Create and start a new thread for the dashboard
|
1020 |
+
dash_thread = threading.Thread(target=run_dash, args=(df, TitleName, Topic_year))
|
1021 |
+
dash_thread.daemon = True
|
1022 |
+
dash_thread.start()
|
1023 |
+
|
1024 |
+
# Open browser automatically
|
1025 |
+
browser_thread = threading.Thread(target=open_browser)
|
1026 |
+
browser_thread.daemon = True
|
1027 |
+
browser_thread.start()
|
1028 |
+
|
1029 |
+
return {
|
1030 |
+
"message": f"Trend analysis completed for papers (page {current_page + 1} of {total_pages})",
|
1031 |
+
"current_page": current_page,
|
1032 |
+
"total_pages": total_pages,
|
1033 |
+
"papers_count": papers_count,
|
1034 |
+
"total_papers": total_papers,
|
1035 |
+
"cluster_sizes": cluster_sizes,
|
1036 |
+
"cluster_titles": topic_labels,
|
1037 |
+
"dashboard_url": f"http://localhost:{DASH_PORT}"
|
1038 |
+
}
|
1039 |
+
|
1040 |
+
|
1041 |
+
# Function to open browser after a short delay
|
1042 |
+
def open_browser():
|
1043 |
+
time.sleep(2) # Wait for servers to start
|
1044 |
+
webbrowser.open_new(f"http://localhost:{DASH_PORT}")
|
app.py
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException, APIRouter, Request
|
2 |
+
from fastapi.responses import HTMLResponse
|
3 |
+
import uvicorn
|
4 |
+
from dataApi import router as dataAPI_router
|
5 |
+
from TrendAnalysis import router as trend_analysis_process
|
6 |
+
from datacite import router as citation_analysis_process
|
7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
8 |
+
from dbconnect import db_Connect
|
9 |
+
from fastapi.templating import Jinja2Templates
|
10 |
+
from fastapi.staticfiles import StaticFiles
|
11 |
+
from venuAnalysis import router as venue_analysis_process
|
12 |
+
from venuedata import router as venuedata_router
|
13 |
+
|
14 |
+
|
15 |
+
|
16 |
+
# Initialize FastAPI app
|
17 |
+
app = FastAPI()
|
18 |
+
|
19 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
20 |
+
app.mount("/assets", StaticFiles(directory="assets"), name="assets")
|
21 |
+
|
22 |
+
templates = Jinja2Templates(directory="templates")
|
23 |
+
templates.env.auto_reload = True
|
24 |
+
|
25 |
+
app.add_middleware(
|
26 |
+
CORSMiddleware,
|
27 |
+
allow_origins=["*"], # For development - restrict this in production
|
28 |
+
allow_credentials=True,
|
29 |
+
allow_methods=["*"],
|
30 |
+
allow_headers=["*"],
|
31 |
+
)
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
# # Include routers
|
37 |
+
app.include_router(dataAPI_router)
|
38 |
+
app.include_router(trend_analysis_process)
|
39 |
+
app.include_router(citation_analysis_process)
|
40 |
+
app.include_router(venue_analysis_process)
|
41 |
+
app.include_router(venuedata_router)
|
42 |
+
|
43 |
+
|
44 |
+
collection,collection1,collection2 = db_Connect()
|
45 |
+
app.state.collection = collection
|
46 |
+
app.state.collection1 = collection1
|
47 |
+
app.state.collection2 = collection2
|
48 |
+
|
49 |
+
|
50 |
+
|
51 |
+
|
52 |
+
# Root endpoint
|
53 |
+
@app.get("/")
|
54 |
+
async def root():
|
55 |
+
return {"message": "Welcome to the Science Mapping Tool!"}
|
56 |
+
|
57 |
+
|
58 |
+
|
59 |
+
@app.get("/home", response_class=HTMLResponse)
|
60 |
+
async def home(request: Request):
|
61 |
+
return templates.TemplateResponse("homepage.html", {"request": request})
|
62 |
+
|
63 |
+
@app.get("/login", response_class=HTMLResponse)
|
64 |
+
async def login(request: Request):
|
65 |
+
return templates.TemplateResponse("loginpage.html", {"request": request})
|
66 |
+
|
67 |
+
@app.get("/contact", response_class=HTMLResponse)
|
68 |
+
async def login(request: Request):
|
69 |
+
return templates.TemplateResponse("contactBoard.html", {"request": request})
|
70 |
+
|
71 |
+
@app.get("/feedback", response_class=HTMLResponse)
|
72 |
+
async def login(request: Request):
|
73 |
+
return templates.TemplateResponse("feedback.html", {"request": request})
|
74 |
+
|
75 |
+
|
76 |
+
# uvicorn app:app --host 0.0.0.0 --port 3000 --reload
|
77 |
+
|
dataApi.py
ADDED
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException, APIRouter, Request
|
2 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
3 |
+
from pydantic import BaseModel
|
4 |
+
import requests
|
5 |
+
import spacy
|
6 |
+
import time
|
7 |
+
import torch
|
8 |
+
from transformers import AutoTokenizer
|
9 |
+
from adapters import AutoAdapterModel
|
10 |
+
from typing import Optional
|
11 |
+
|
12 |
+
router = APIRouter()
|
13 |
+
|
14 |
+
|
15 |
+
# Load spaCy model
|
16 |
+
nlp = spacy.load("en_core_sci_sm")
|
17 |
+
|
18 |
+
# Pydantic models for request/responsecl
|
19 |
+
class ResearchQuery(BaseModel):
|
20 |
+
userId: str # User ID
|
21 |
+
topic: str # Research topic
|
22 |
+
year: str # Year of publication
|
23 |
+
|
24 |
+
|
25 |
+
class PaperMetadata(BaseModel):
|
26 |
+
paperId: str
|
27 |
+
title: str
|
28 |
+
abstract: str
|
29 |
+
citationCount: int
|
30 |
+
influentialCitationCount: int
|
31 |
+
publicationDate: str
|
32 |
+
url: str
|
33 |
+
|
34 |
+
|
35 |
+
# Extract keywords from the research topic
|
36 |
+
def extract_keywords(text):
|
37 |
+
doc = nlp(text.lower()) # Normalize to lowercase
|
38 |
+
|
39 |
+
# Step 1: Extract noun chunks (compound phrases)
|
40 |
+
noun_chunks = [chunk.text for chunk in doc.noun_chunks]
|
41 |
+
|
42 |
+
# Step 2: Extract individual tokens (nouns and verbs)
|
43 |
+
individual_tokens = [
|
44 |
+
token.text
|
45 |
+
for token in doc
|
46 |
+
if token.pos_ in ["NOUN", "VERB"] and not token.is_stop
|
47 |
+
]
|
48 |
+
|
49 |
+
# Step 3: Combine noun chunks and individual tokens
|
50 |
+
keywords = set(noun_chunks + individual_tokens)
|
51 |
+
cleaned_keywords = set()
|
52 |
+
for keyword in keywords:
|
53 |
+
# Check if the keyword is part of any larger noun chunk
|
54 |
+
if not any(keyword in chunk for chunk in noun_chunks if keyword != chunk):
|
55 |
+
cleaned_keywords.add(keyword)
|
56 |
+
|
57 |
+
return sorted(list(cleaned_keywords))
|
58 |
+
|
59 |
+
|
60 |
+
# Construct query based on keywords
|
61 |
+
def construct_query(keywords):
|
62 |
+
query = " + ".join(keywords)
|
63 |
+
return query
|
64 |
+
|
65 |
+
|
66 |
+
# Fetch Paper IDs using Semantic Scholar Bulk Search API
|
67 |
+
def fetch_paper_ids(query, year):
|
68 |
+
search_url = "https://api.semanticscholar.org/graph/v1/paper/search/bulk"
|
69 |
+
search_params = {
|
70 |
+
"query": query,
|
71 |
+
"year": year,
|
72 |
+
"fields": "paperId",
|
73 |
+
}
|
74 |
+
response = requests.get(search_url, params=search_params)
|
75 |
+
|
76 |
+
if response.status_code == 200:
|
77 |
+
data = response.json()
|
78 |
+
papers = data.get("data", [])
|
79 |
+
paper_ids = [paper["paperId"] for paper in papers]
|
80 |
+
return paper_ids
|
81 |
+
else:
|
82 |
+
raise HTTPException(status_code=response.status_code, detail="Error fetching paper IDs")
|
83 |
+
|
84 |
+
|
85 |
+
# Fetch metadata using Semantic Scholar Graph API
|
86 |
+
def fetch_metadata(batch_ids):
|
87 |
+
graph_url = "https://api.semanticscholar.org/graph/v1/paper/batch"
|
88 |
+
metadata_params = {
|
89 |
+
"fields": "title,abstract,citationCount,influentialCitationCount,publicationDate,url"
|
90 |
+
}
|
91 |
+
|
92 |
+
attempt = 0
|
93 |
+
max_retries = 20
|
94 |
+
|
95 |
+
while attempt < max_retries:
|
96 |
+
response = requests.post(graph_url, json={"ids": batch_ids}, params=metadata_params)
|
97 |
+
|
98 |
+
if response.status_code == 200:
|
99 |
+
return response.json()
|
100 |
+
|
101 |
+
elif response.status_code == 429:
|
102 |
+
wait_time = 5
|
103 |
+
print(f"429 Too Many Requests. Retrying in {wait_time} seconds...")
|
104 |
+
time.sleep(wait_time)
|
105 |
+
attempt += 1
|
106 |
+
|
107 |
+
else:
|
108 |
+
raise HTTPException(status_code=response.status_code, detail="Error fetching metadata")
|
109 |
+
|
110 |
+
raise HTTPException(status_code=500, detail="Max retries reached while fetching metadata")
|
111 |
+
|
112 |
+
|
113 |
+
# Clean and process metadata using SPECTER2 embeddings
|
114 |
+
def clean_and_process_metadata(metadata_list):
|
115 |
+
# Load tokenizer and base model
|
116 |
+
tokenizer = AutoTokenizer.from_pretrained("allenai/specter2_base")
|
117 |
+
model = AutoAdapterModel.from_pretrained("allenai/specter2_base")
|
118 |
+
|
119 |
+
# Load and activate the proximity adapter
|
120 |
+
model.load_adapter("allenai/specter2", source="hf", load_as="proximity", set_active=True)
|
121 |
+
|
122 |
+
# Move model to GPU if available
|
123 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
124 |
+
model.to(device)
|
125 |
+
|
126 |
+
cleaned_metadata = []
|
127 |
+
invalid_papers = []
|
128 |
+
|
129 |
+
for paper in metadata_list:
|
130 |
+
paper_id = paper.get("paperId")
|
131 |
+
title = paper.get("title")
|
132 |
+
abstract = paper.get("abstract")
|
133 |
+
|
134 |
+
# Case 1: Paper lacks sufficient content (title + abstract or just abstract)
|
135 |
+
if not (title and abstract) and not abstract:
|
136 |
+
invalid_papers.append(paper_id)
|
137 |
+
continue
|
138 |
+
|
139 |
+
# Prepare text for embedding generation
|
140 |
+
text = title + tokenizer.sep_token + abstract if title and abstract else abstract
|
141 |
+
|
142 |
+
# Tokenize and encode
|
143 |
+
inputs = tokenizer(text, padding=True, truncation=True, return_tensors="pt", max_length=512)
|
144 |
+
inputs = {k: v.to(device) for k, v in inputs.items()} # Move inputs to device
|
145 |
+
|
146 |
+
with torch.no_grad():
|
147 |
+
output = model(**inputs)
|
148 |
+
embedding = output.last_hidden_state[:, 0, :].cpu().numpy().tolist()
|
149 |
+
|
150 |
+
# Add embedding to the paper metadata
|
151 |
+
paper["embedding"] = {"model": "specter2_proximity", "vector": embedding}
|
152 |
+
|
153 |
+
# Add the cleaned paper to the list
|
154 |
+
cleaned_metadata.append(paper)
|
155 |
+
|
156 |
+
return cleaned_metadata, invalid_papers
|
157 |
+
|
158 |
+
|
159 |
+
# Save metadata to MongoDB under username and topic
|
160 |
+
async def save_to_mongodb(userId: str, topic: str, year:str , metadata_list, request:Request):
|
161 |
+
# Create or update the document for the user-topic combination
|
162 |
+
collection = request.app.state.collection
|
163 |
+
|
164 |
+
await collection.update_one(
|
165 |
+
{"userId": userId, "topic": topic ,"year": year}, # Filter by user and topic
|
166 |
+
{"$set": {"papers": metadata_list}}, # Update the papers field
|
167 |
+
upsert=True # Create the document if it doesn't exist
|
168 |
+
)
|
169 |
+
|
170 |
+
print(f"Saved {len(metadata_list)} papers for user '{userId}' and topic '{topic}'")
|
171 |
+
|
172 |
+
|
173 |
+
@router.get("/test")
|
174 |
+
def greet():
|
175 |
+
return {
|
176 |
+
"message":"helllo jessi"
|
177 |
+
}
|
178 |
+
|
179 |
+
|
180 |
+
# Endpoint to process user input and fetch data
|
181 |
+
@router.post("/analyze")
|
182 |
+
async def analyze(query: ResearchQuery,requests:Request):
|
183 |
+
# Extract keywords and construct query
|
184 |
+
keywords = extract_keywords(query.topic)
|
185 |
+
refined_query = construct_query(keywords)
|
186 |
+
|
187 |
+
print(f"\n🔍 Refined Query: {refined_query}")
|
188 |
+
|
189 |
+
# Fetch paper IDs
|
190 |
+
paper_ids = fetch_paper_ids(refined_query, query.year)
|
191 |
+
print(f"\nFound {len(paper_ids)} papers for '{query.topic}' in {query.year}\n")
|
192 |
+
|
193 |
+
# Fetch metadata in batches
|
194 |
+
metadata_list = []
|
195 |
+
batch_size = 100
|
196 |
+
|
197 |
+
for i in range(0, len(paper_ids), batch_size):
|
198 |
+
batch_ids = paper_ids[i : i + batch_size]
|
199 |
+
metadata = fetch_metadata(batch_ids)
|
200 |
+
metadata_list.extend(metadata)
|
201 |
+
print(f"Retrieved {len(batch_ids)} papers' metadata...")
|
202 |
+
|
203 |
+
# Clean and process metadata
|
204 |
+
cleaned_metadata, invalid_papers = clean_and_process_metadata(metadata_list)
|
205 |
+
|
206 |
+
# Save cleaned metadata to MongoDB under the username and topic
|
207 |
+
await save_to_mongodb(query.userId, query.topic, query.year, cleaned_metadata,requests )
|
208 |
+
|
209 |
+
return {
|
210 |
+
"message": f"Processed {len(cleaned_metadata)} papers and cleaned data.",
|
211 |
+
"invalid_papers_removed": len(invalid_papers),
|
212 |
+
}
|
213 |
+
|
214 |
+
|
215 |
+
|
216 |
+
# Pydantic model for request data
|
217 |
+
class CheckDataRequest(BaseModel):
|
218 |
+
userId: str
|
219 |
+
topic: str
|
220 |
+
year: Optional[str] = None
|
221 |
+
|
222 |
+
# Endpoint to check if the combination of userId, topic, and year exists
|
223 |
+
|
224 |
+
@router.post("/check-data-exists/")
|
225 |
+
async def check_data_exists(request_data: CheckDataRequest, request:Request):
|
226 |
+
# Create a query to check if the data exists
|
227 |
+
query = {
|
228 |
+
"userId": request_data.userId,
|
229 |
+
"topic": request_data.topic
|
230 |
+
}
|
231 |
+
|
232 |
+
# Add year to query if it's provided
|
233 |
+
if request_data.year:
|
234 |
+
query["year"] = request_data.year
|
235 |
+
|
236 |
+
collection = request.app.state.collection
|
237 |
+
# Check if a document matching the query exists
|
238 |
+
document = await collection.find_one(query) # Await the async operation
|
239 |
+
|
240 |
+
# Return result
|
241 |
+
return {
|
242 |
+
"exists": document is not None,
|
243 |
+
"message": "Data found" if document else "Data not found"
|
244 |
+
}
|
245 |
+
|
datacite.py
ADDED
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import spacy
|
2 |
+
import requests
|
3 |
+
from time import sleep
|
4 |
+
from tqdm import tqdm
|
5 |
+
from fastapi import HTTPException
|
6 |
+
from pydantic import BaseModel
|
7 |
+
from concurrent.futures import ThreadPoolExecutor
|
8 |
+
from datetime import datetime
|
9 |
+
from fastapi import APIRouter, Request
|
10 |
+
from fastapi.templating import Jinja2Templates
|
11 |
+
from fastapi.responses import HTMLResponse
|
12 |
+
import json
|
13 |
+
|
14 |
+
# Load spaCy model
|
15 |
+
nlp = spacy.load("en_core_sci_sm")
|
16 |
+
templates = Jinja2Templates(directory="templates")
|
17 |
+
# Constants
|
18 |
+
OPENALEX_API_URL = "https://api.openalex.org/works"
|
19 |
+
PER_PAGE = 100
|
20 |
+
REQUEST_DELAY = 0.5
|
21 |
+
MAX_RESULTS = 1000
|
22 |
+
|
23 |
+
router = APIRouter()
|
24 |
+
|
25 |
+
|
26 |
+
# Pydantic Models
|
27 |
+
class CitationAnalysisRequest(BaseModel):
|
28 |
+
userId: str
|
29 |
+
topic: str
|
30 |
+
year: int
|
31 |
+
|
32 |
+
|
33 |
+
# 1. Keyword Extraction
|
34 |
+
def extract_keywords(text):
|
35 |
+
doc = nlp(text.lower())
|
36 |
+
noun_chunks = [chunk.text for chunk in doc.noun_chunks]
|
37 |
+
individual_tokens = [token.text for token in doc if token.pos_ in ["NOUN", "VERB"] and not token.is_stop]
|
38 |
+
keywords = set(noun_chunks + individual_tokens)
|
39 |
+
|
40 |
+
cleaned_keywords = set()
|
41 |
+
for keyword in keywords:
|
42 |
+
if not any(keyword in chunk for chunk in noun_chunks if keyword != chunk):
|
43 |
+
cleaned_keywords.add(keyword)
|
44 |
+
return sorted(list(cleaned_keywords))
|
45 |
+
|
46 |
+
|
47 |
+
# 2. Citation Batch Fetching
|
48 |
+
def fetch_citing_papers_for_batch(paper_ids):
|
49 |
+
citing_map = {pid: [] for pid in paper_ids}
|
50 |
+
batch_size = 100
|
51 |
+
|
52 |
+
def fetch_batch(batch_ids):
|
53 |
+
filter_query = "|".join(batch_ids)
|
54 |
+
cursor = "*"
|
55 |
+
local_citing_map = {pid: [] for pid in batch_ids}
|
56 |
+
|
57 |
+
while True:
|
58 |
+
params = {
|
59 |
+
"filter": f"cites:{filter_query}",
|
60 |
+
"per_page": 200,
|
61 |
+
"cursor": cursor
|
62 |
+
}
|
63 |
+
|
64 |
+
try:
|
65 |
+
response = requests.get(OPENALEX_API_URL, params=params)
|
66 |
+
if response.status_code != 200:
|
67 |
+
print(f"❌ Error while fetching citing papers: {response.status_code}")
|
68 |
+
break
|
69 |
+
|
70 |
+
data = response.json()
|
71 |
+
for citing_work in data.get("results", []):
|
72 |
+
refs = citing_work.get("referenced_works", [])
|
73 |
+
for ref_id in refs:
|
74 |
+
if ref_id in local_citing_map:
|
75 |
+
local_citing_map[ref_id].append(citing_work["id"])
|
76 |
+
|
77 |
+
next_cursor = data.get("meta", {}).get("next_cursor")
|
78 |
+
if not next_cursor:
|
79 |
+
break
|
80 |
+
cursor = next_cursor
|
81 |
+
sleep(REQUEST_DELAY)
|
82 |
+
|
83 |
+
except Exception as e:
|
84 |
+
print(f"❌ Exception: {e}")
|
85 |
+
break
|
86 |
+
|
87 |
+
return local_citing_map
|
88 |
+
|
89 |
+
print("\n🔗 Fetching citing papers for all topic papers...")
|
90 |
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
91 |
+
futures = []
|
92 |
+
for i in range(0, len(paper_ids), batch_size):
|
93 |
+
batch_ids = paper_ids[i:i + batch_size]
|
94 |
+
futures.append(executor.submit(fetch_batch, batch_ids))
|
95 |
+
|
96 |
+
for future in tqdm(futures, desc="ParallelGroup"):
|
97 |
+
local_citing_map = future.result()
|
98 |
+
citing_map.update(local_citing_map)
|
99 |
+
|
100 |
+
return citing_map
|
101 |
+
|
102 |
+
|
103 |
+
# 3. Fetch topic papers
|
104 |
+
def fetch_papers_with_citations(keywords, year, concept_threshold=0.8, max_results=MAX_RESULTS):
|
105 |
+
query = " + ".join(keywords)
|
106 |
+
print(f"\n🔍 Final OpenAlex Query: {query}")
|
107 |
+
params = {
|
108 |
+
"filter": f"title_and_abstract.search:{query},publication_year:{year}",
|
109 |
+
"per_page": PER_PAGE,
|
110 |
+
"cursor": "*"
|
111 |
+
}
|
112 |
+
|
113 |
+
all_papers = []
|
114 |
+
paper_id_map = {}
|
115 |
+
print("\n🚀 Fetching topic papers from OpenAlex...")
|
116 |
+
|
117 |
+
while len(all_papers) < max_results:
|
118 |
+
response = requests.get(OPENALEX_API_URL, params=params)
|
119 |
+
if response.status_code != 200:
|
120 |
+
print(f"❌ Error: {response.status_code}")
|
121 |
+
break
|
122 |
+
|
123 |
+
data = response.json()
|
124 |
+
results = data.get("results", [])
|
125 |
+
|
126 |
+
for paper in tqdm(results, desc="📄 Collecting papers"):
|
127 |
+
paper_id = paper.get("id", "")
|
128 |
+
concepts = [
|
129 |
+
c["display_name"]
|
130 |
+
for c in paper.get("concepts", [])
|
131 |
+
if c.get("score", 0) >= concept_threshold
|
132 |
+
]
|
133 |
+
|
134 |
+
paper_data = {
|
135 |
+
"id": paper_id,
|
136 |
+
"title": paper.get("title", "No title"),
|
137 |
+
"cited_by_count": paper.get("cited_by_count", 0),
|
138 |
+
"publication_date": paper.get("publication_date", ""),
|
139 |
+
"referenced_works": paper.get("referenced_works", []),
|
140 |
+
"concepts": concepts,
|
141 |
+
"cited_by_ids": [] # to be filled later
|
142 |
+
}
|
143 |
+
paper_id_map[paper_id] = paper_data
|
144 |
+
all_papers.append(paper_data)
|
145 |
+
|
146 |
+
next_cursor = data.get("meta", {}).get("next_cursor")
|
147 |
+
if not next_cursor:
|
148 |
+
break
|
149 |
+
|
150 |
+
params["cursor"] = next_cursor
|
151 |
+
sleep(REQUEST_DELAY)
|
152 |
+
|
153 |
+
all_ids = list(paper_id_map.keys())
|
154 |
+
citing_map = fetch_citing_papers_for_batch(all_ids)
|
155 |
+
|
156 |
+
for pid, citing_ids in citing_map.items():
|
157 |
+
paper_id_map[pid]["cited_by_ids"] = citing_ids
|
158 |
+
|
159 |
+
cleaned_papers = []
|
160 |
+
for paper in all_papers:
|
161 |
+
if paper.get("referenced_works") or paper.get("cited_by_ids"):
|
162 |
+
cleaned_papers.append(paper)
|
163 |
+
|
164 |
+
print(f"\n🧹 Removed {len(all_papers) - len(cleaned_papers)} papers without references or citations.")
|
165 |
+
return cleaned_papers[:max_results]
|
166 |
+
|
167 |
+
|
168 |
+
# 4. Save to MongoDB
|
169 |
+
async def save_to_mongodb(userId, topic, year, papers, request: Request):
|
170 |
+
metadata = {
|
171 |
+
"userId": userId,
|
172 |
+
"topic": topic,
|
173 |
+
"year": year,
|
174 |
+
"scraped_on": datetime.utcnow().isoformat() + "Z",
|
175 |
+
"papers": papers
|
176 |
+
}
|
177 |
+
collection = request.app.state.collection1
|
178 |
+
result = await collection.insert_one(metadata)
|
179 |
+
print(f"\n✅ Saved metadata to MongoDB with ID: {result.inserted_id}")
|
180 |
+
return str(result.inserted_id)
|
181 |
+
|
182 |
+
|
183 |
+
# 5. FastAPI Endpoints
|
184 |
+
@router.post("/save")
|
185 |
+
async def save_data(data_request: CitationAnalysisRequest, saveRequest: Request):
|
186 |
+
userId = data_request.userId
|
187 |
+
topic = data_request.topic
|
188 |
+
year = data_request.year
|
189 |
+
|
190 |
+
keywords = extract_keywords(topic)
|
191 |
+
print("\n🔑 Extracted Keywords:")
|
192 |
+
print(keywords)
|
193 |
+
|
194 |
+
if not keywords:
|
195 |
+
raise HTTPException(status_code=400, detail="No keywords extracted. Please provide a valid topic.")
|
196 |
+
|
197 |
+
papers = fetch_papers_with_citations(keywords, year)
|
198 |
+
if not papers:
|
199 |
+
raise HTTPException(status_code=404, detail="No papers retrieved for the given topic and year.")
|
200 |
+
|
201 |
+
document_id = await save_to_mongodb(userId, topic, year, papers, saveRequest)
|
202 |
+
return {"message": "Data saved successfully!", "document_id": document_id}
|
203 |
+
|
204 |
+
|
205 |
+
# NEW ENDPOINT: Get citation data from MongoDB
|
206 |
+
# @router.post("/citation-data",response_class=HTMLResponse)
|
207 |
+
# async def get_citation_data(data_request:CitationAnalysisRequest , saveRequest: Request):
|
208 |
+
# # Build the query based on provided parameters
|
209 |
+
# query = {"userId": data_request.userId}
|
210 |
+
|
211 |
+
# if data_request.topic:
|
212 |
+
# query["topic"] = data_request.topic
|
213 |
+
|
214 |
+
# if data_request.year:
|
215 |
+
# query["year"] = data_request.year
|
216 |
+
|
217 |
+
# collection = saveRequest.app.state.collection1
|
218 |
+
# result = await collection.find_one(
|
219 |
+
# query,
|
220 |
+
# sort=[("scraped_on", 1)] # Sort by scraped_on in descending order to get the most recent
|
221 |
+
# )
|
222 |
+
|
223 |
+
# if not result:
|
224 |
+
# raise HTTPException(status_code=404, detail="No data found for the specified criteria")
|
225 |
+
|
226 |
+
# # Convert ObjectId to string for JSON serialization
|
227 |
+
# result["_id"] = str(result["_id"])
|
228 |
+
|
229 |
+
# data = result["papers"]
|
230 |
+
# data_json = json.dumps(data)
|
231 |
+
# print(" i am here")
|
232 |
+
# return templates.TemplateResponse("gra.html", {"request": saveRequest,"data":data_json})
|
233 |
+
|
234 |
+
|
235 |
+
@router.get("/citation-data", response_class=HTMLResponse)
|
236 |
+
async def get_citation_data(request: Request):
|
237 |
+
# Extract query parameters
|
238 |
+
user_id = request.query_params.get("userId")
|
239 |
+
topic = request.query_params.get("topic")
|
240 |
+
year = request.query_params.get("year")
|
241 |
+
|
242 |
+
if not user_id or not topic or not year:
|
243 |
+
raise HTTPException(status_code=400, detail="Missing required query parameters.")
|
244 |
+
|
245 |
+
# Build the query based on provided parameters
|
246 |
+
query = {"userId": user_id, "topic": topic, "year": int(year)}
|
247 |
+
|
248 |
+
collection = request.app.state.collection1
|
249 |
+
result = await collection.find_one(
|
250 |
+
query,
|
251 |
+
sort=[("scraped_on", -1)] # Sort by scraped_on in descending order to get the most recent
|
252 |
+
)
|
253 |
+
|
254 |
+
if not result:
|
255 |
+
raise HTTPException(status_code=404, detail="No data found for the specified criteria")
|
256 |
+
|
257 |
+
# Convert ObjectId to string for JSON serialization
|
258 |
+
result["_id"] = str(result["_id"])
|
259 |
+
|
260 |
+
data = result["papers"]
|
261 |
+
if not data:
|
262 |
+
raise HTTPException(status_code=404, detail="No papers found in the database.")
|
263 |
+
|
264 |
+
data_json = json.dumps(data)
|
265 |
+
|
266 |
+
return templates.TemplateResponse("gra.html", {"request": request, "data": data_json})
|
267 |
+
|
268 |
+
|
269 |
+
@router.post("/check-data-exists-citation/")
|
270 |
+
async def check_data_exists(data_request: CitationAnalysisRequest, saveRequest: Request):
|
271 |
+
# Build the query based on provided parameters
|
272 |
+
query = {
|
273 |
+
"userId": data_request.userId,
|
274 |
+
"topic": data_request.topic,
|
275 |
+
"year": data_request.year
|
276 |
+
}
|
277 |
+
|
278 |
+
# Access the MongoDB collection from the app state
|
279 |
+
collection = saveRequest.app.state.collection1
|
280 |
+
|
281 |
+
# Check if a document matching the query exists
|
282 |
+
document = await collection.find_one(query)
|
283 |
+
|
284 |
+
# Return the result
|
285 |
+
if document:
|
286 |
+
return {
|
287 |
+
"exists": True,
|
288 |
+
"message": "Data found for the given userId, topic, and year."
|
289 |
+
}
|
290 |
+
else:
|
291 |
+
return {
|
292 |
+
"exists": False,
|
293 |
+
"message": "No data found for the given userId, topic, and year."
|
294 |
+
}
|
dbconnect.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
import os
|
3 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
4 |
+
load_dotenv()
|
5 |
+
|
6 |
+
|
7 |
+
def db_Connect():
|
8 |
+
MONGO_URI = os.getenv("MONGO_URI")
|
9 |
+
DATABASE_NAME = "PaperLens_Db"
|
10 |
+
COLLECTION_NAME = "Trend_Analysis_Data"
|
11 |
+
COLLECTION_NAME1 = "Citation_Analysis_Data"
|
12 |
+
COLLECTION_NAME2 = "Venue_Analysis_Data"
|
13 |
+
client = AsyncIOMotorClient(MONGO_URI)
|
14 |
+
db = client[DATABASE_NAME]
|
15 |
+
collection = db[COLLECTION_NAME]
|
16 |
+
collection1 = db[COLLECTION_NAME1]
|
17 |
+
collection2 = db[COLLECTION_NAME2]
|
18 |
+
return collection,collection1,collection2
|
19 |
+
|
20 |
+
|
21 |
+
|
22 |
+
|
23 |
+
|
static/css/dashboard.css
ADDED
File without changes
|
static/css/gra.css
ADDED
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root{
|
2 |
+
--secondary-color: #fcee0a;
|
3 |
+
--text-color: #333;
|
4 |
+
--bg:#4dd9ca;
|
5 |
+
}
|
6 |
+
body {
|
7 |
+
font-family: Arial, sans-serif;
|
8 |
+
margin: 0;
|
9 |
+
padding: 0;
|
10 |
+
background-color:var( --text-color);
|
11 |
+
}
|
12 |
+
.concept-label {
|
13 |
+
font-size: 14px;
|
14 |
+
color: #555;
|
15 |
+
margin-bottom: 5px;
|
16 |
+
font-weight: 500;
|
17 |
+
}
|
18 |
+
|
19 |
+
#container {
|
20 |
+
width: 100%;
|
21 |
+
height: 100vh;
|
22 |
+
overflow: hidden;
|
23 |
+
}
|
24 |
+
|
25 |
+
svg {
|
26 |
+
width: 100%;
|
27 |
+
height: 100%;
|
28 |
+
}
|
29 |
+
|
30 |
+
.node {
|
31 |
+
cursor: pointer;
|
32 |
+
}
|
33 |
+
|
34 |
+
.link {
|
35 |
+
stroke-opacity: 0.6;
|
36 |
+
}
|
37 |
+
|
38 |
+
.tooltip {
|
39 |
+
position: absolute;
|
40 |
+
padding: 10px;
|
41 |
+
background-color: rgba(255, 255, 255, 0.95);
|
42 |
+
border: 1px solid #ddd;
|
43 |
+
border-radius: 5px;
|
44 |
+
pointer-events: none;
|
45 |
+
font-size: 14px;
|
46 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
47 |
+
max-width: 300px;
|
48 |
+
line-height: 1.4;
|
49 |
+
z-index: 1000;
|
50 |
+
}
|
51 |
+
|
52 |
+
.tooltip strong {
|
53 |
+
display: block;
|
54 |
+
margin-bottom: 5px;
|
55 |
+
font-size: 15px;
|
56 |
+
color: #333;
|
57 |
+
}
|
58 |
+
|
59 |
+
.tooltip em {
|
60 |
+
color: #4285f4;
|
61 |
+
font-style: italic;
|
62 |
+
}
|
63 |
+
|
64 |
+
#paperConcepts {
|
65 |
+
margin-top: 15px;
|
66 |
+
display: flex;
|
67 |
+
flex-wrap: wrap;
|
68 |
+
gap: 5px;
|
69 |
+
}
|
70 |
+
|
71 |
+
.controls {
|
72 |
+
position: absolute;
|
73 |
+
top: 20px;
|
74 |
+
left: 20px;
|
75 |
+
background-color: var(--bg);
|
76 |
+
padding: 15px;
|
77 |
+
border-radius: 5px;
|
78 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
79 |
+
display: flex;
|
80 |
+
flex-direction: column;
|
81 |
+
gap: 10px;
|
82 |
+
z-index: 100;
|
83 |
+
max-height: 80vh;
|
84 |
+
overflow-y: auto;
|
85 |
+
border: 8px solid #000; /* Add this line for a black border */
|
86 |
+
}
|
87 |
+
|
88 |
+
.control-section {
|
89 |
+
border-top: 1px solid #eee;
|
90 |
+
padding-top: 10px;
|
91 |
+
margin-top: 10px;
|
92 |
+
}
|
93 |
+
|
94 |
+
.control-section h4 {
|
95 |
+
margin-top: 0;
|
96 |
+
margin-bottom: 10px;
|
97 |
+
color: #333;
|
98 |
+
font-weight: bolder;
|
99 |
+
font-family: Georgia, 'Times New Roman', Times, serif;
|
100 |
+
}
|
101 |
+
|
102 |
+
.controls label {
|
103 |
+
display: flex;
|
104 |
+
align-items: center;
|
105 |
+
gap: 10px;
|
106 |
+
margin-bottom: 8px;
|
107 |
+
font-style: italic;
|
108 |
+
font-family:'Times New Roman', Times, serif;
|
109 |
+
}
|
110 |
+
|
111 |
+
.controls input {
|
112 |
+
width: 100px;
|
113 |
+
}
|
114 |
+
|
115 |
+
button {
|
116 |
+
margin-top: 10px;
|
117 |
+
padding: 8px 12px;
|
118 |
+
background-color: black;
|
119 |
+
color: white;
|
120 |
+
border: none;
|
121 |
+
border-radius: 4px;
|
122 |
+
cursor: pointer;
|
123 |
+
}
|
124 |
+
|
125 |
+
button:hover {
|
126 |
+
background-color: #3b78e7;
|
127 |
+
}
|
128 |
+
|
129 |
+
/* Improved legend styling */
|
130 |
+
.legend {
|
131 |
+
position: absolute;
|
132 |
+
right: 20px;
|
133 |
+
top: 20px;
|
134 |
+
background-color: var(--bg);
|
135 |
+
padding: 15px;
|
136 |
+
border-radius: 5px;
|
137 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
138 |
+
z-index: 100;
|
139 |
+
max-width: 250px;
|
140 |
+
border: 8px solid #000; /* Add this line for a black border */
|
141 |
+
}
|
142 |
+
|
143 |
+
.legend h3 {
|
144 |
+
margin-top: 0;
|
145 |
+
margin-bottom: 12px;
|
146 |
+
font-family: Georgia, 'Times New Roman', Times, serif;
|
147 |
+
}
|
148 |
+
|
149 |
+
|
150 |
+
.legend-item {
|
151 |
+
display: flex;
|
152 |
+
align-items: center;
|
153 |
+
margin-bottom: 10px;
|
154 |
+
height: 24px; /* Standardize height for all items */
|
155 |
+
font-family: 'Times New Roman', Times, serif;
|
156 |
+
}
|
157 |
+
|
158 |
+
/* Standardize all legend indicators */
|
159 |
+
.legend-indicator {
|
160 |
+
width: 32px; /* Increased from 24px */
|
161 |
+
height: 32px; /* Increased from 24px */
|
162 |
+
margin-right: 10px;
|
163 |
+
display: flex;
|
164 |
+
align-items: center;
|
165 |
+
justify-content: center;
|
166 |
+
flex-shrink: 0;
|
167 |
+
}
|
168 |
+
|
169 |
+
/* Specific styles for different shapes */
|
170 |
+
.legend-circle {
|
171 |
+
border-radius: 50%;
|
172 |
+
}
|
173 |
+
|
174 |
+
.legend-circle.main {
|
175 |
+
width: 16px;
|
176 |
+
height: 16px;
|
177 |
+
background-color: #4285f4;
|
178 |
+
}
|
179 |
+
|
180 |
+
.legend-circle.referenced {
|
181 |
+
width: 12px;
|
182 |
+
height: 12px;
|
183 |
+
background-color: #34a853;
|
184 |
+
}
|
185 |
+
|
186 |
+
.legend-circle.citing {
|
187 |
+
width: 12px;
|
188 |
+
height: 12px;
|
189 |
+
background-color: #ea4335;
|
190 |
+
}
|
191 |
+
|
192 |
+
.legend-diamond {
|
193 |
+
width: 12px;
|
194 |
+
height: 12px;
|
195 |
+
background-color: #fbbc05;
|
196 |
+
transform: rotate(45deg);
|
197 |
+
}
|
198 |
+
|
199 |
+
.legend-star svg {
|
200 |
+
width: 20px;
|
201 |
+
height: 20px;
|
202 |
+
}
|
203 |
+
|
204 |
+
.legend-arrow {
|
205 |
+
width: 50px;
|
206 |
+
height: 20px;
|
207 |
+
}
|
208 |
+
|
209 |
+
.legend-info {
|
210 |
+
font-weight: bold;
|
211 |
+
color: #555;
|
212 |
+
margin-top: 10px;
|
213 |
+
padding-top: 10px;
|
214 |
+
border-top: 1px solid #eee;
|
215 |
+
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
|
216 |
+
}
|
217 |
+
|
218 |
+
.legend-info p {
|
219 |
+
margin: 5px 0;
|
220 |
+
}
|
221 |
+
|
222 |
+
|
223 |
+
#loadingIndicator {
|
224 |
+
position: absolute;
|
225 |
+
top: 50%;
|
226 |
+
left: 50%;
|
227 |
+
transform: translate(-50%, -50%);
|
228 |
+
background-color: rgba(255, 255, 255, 0.9);
|
229 |
+
padding: 20px;
|
230 |
+
border-radius: 5px;
|
231 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
232 |
+
z-index: 1000;
|
233 |
+
display: none;
|
234 |
+
}
|
235 |
+
|
236 |
+
/* New styles for paper details panel */
|
237 |
+
.paper-details-panel {
|
238 |
+
position: absolute;
|
239 |
+
top: 20px;
|
240 |
+
right: 20px;
|
241 |
+
background-color: white;
|
242 |
+
width: 350px;
|
243 |
+
max-height: 80vh;
|
244 |
+
border-radius: 5px;
|
245 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
246 |
+
z-index: 200;
|
247 |
+
display: none;
|
248 |
+
overflow: hidden;
|
249 |
+
transition: transform 0.3s ease;
|
250 |
+
}
|
251 |
+
|
252 |
+
.panel-header {
|
253 |
+
display: flex;
|
254 |
+
justify-content: space-between;
|
255 |
+
align-items: center;
|
256 |
+
padding: 12px 15px;
|
257 |
+
background-color: #4285f4;
|
258 |
+
color: white;
|
259 |
+
}
|
260 |
+
|
261 |
+
.panel-header h3 {
|
262 |
+
margin: 0;
|
263 |
+
font-size: 16px;
|
264 |
+
}
|
265 |
+
|
266 |
+
.close-button {
|
267 |
+
background: none;
|
268 |
+
border: none;
|
269 |
+
color: white;
|
270 |
+
font-size: 24px;
|
271 |
+
cursor: pointer;
|
272 |
+
margin: 0;
|
273 |
+
padding: 0;
|
274 |
+
}
|
275 |
+
|
276 |
+
.panel-content {
|
277 |
+
padding: 15px;
|
278 |
+
overflow-y: auto;
|
279 |
+
max-height: calc(80vh - 50px);
|
280 |
+
}
|
281 |
+
|
282 |
+
#paperInfo {
|
283 |
+
margin-bottom: 20px;
|
284 |
+
padding-bottom: 15px;
|
285 |
+
border-bottom: 1px solid #eee;
|
286 |
+
}
|
287 |
+
|
288 |
+
#paperTitle {
|
289 |
+
margin-top: 0;
|
290 |
+
margin-bottom: 10px;
|
291 |
+
font-size: 16px;
|
292 |
+
color: #333;
|
293 |
+
}
|
294 |
+
|
295 |
+
#paperType, #paperCitations {
|
296 |
+
margin: 5px 0;
|
297 |
+
font-size: 14px;
|
298 |
+
color: #555;
|
299 |
+
}
|
300 |
+
|
301 |
+
#paperConcepts {
|
302 |
+
margin-top: 10px;
|
303 |
+
}
|
304 |
+
|
305 |
+
.concept-tag {
|
306 |
+
display: inline-block;
|
307 |
+
padding: 4px 8px;
|
308 |
+
margin: 3px;
|
309 |
+
background-color: #e8f0fe;
|
310 |
+
color: #4285f4;
|
311 |
+
border-radius: 12px;
|
312 |
+
font-size: 12px;
|
313 |
+
}
|
314 |
+
|
315 |
+
.paper-link {
|
316 |
+
margin-top: 15px;
|
317 |
+
}
|
318 |
+
|
319 |
+
.paper-link a {
|
320 |
+
color: #4285f4;
|
321 |
+
text-decoration: none;
|
322 |
+
font-weight: bold;
|
323 |
+
}
|
324 |
+
|
325 |
+
.paper-link a:hover {
|
326 |
+
text-decoration: underline;
|
327 |
+
}
|
328 |
+
|
329 |
+
.paper-lists {
|
330 |
+
display: flex;
|
331 |
+
flex-direction: column;
|
332 |
+
gap: 20px;
|
333 |
+
}
|
334 |
+
|
335 |
+
.paper-list h5 {
|
336 |
+
margin-top: 0;
|
337 |
+
margin-bottom: 10px;
|
338 |
+
font-size: 14px;
|
339 |
+
color: #333;
|
340 |
+
}
|
341 |
+
|
342 |
+
.paper-list ul {
|
343 |
+
margin: 0;
|
344 |
+
padding: 0;
|
345 |
+
list-style: none;
|
346 |
+
max-height: 200px;
|
347 |
+
overflow-y: auto;
|
348 |
+
}
|
349 |
+
|
350 |
+
.paper-list li {
|
351 |
+
margin-bottom: 8px;
|
352 |
+
padding-bottom: 8px;
|
353 |
+
border-bottom: 1px solid #f0f0f0;
|
354 |
+
}
|
355 |
+
|
356 |
+
.paper-list li:last-child {
|
357 |
+
border-bottom: none;
|
358 |
+
}
|
359 |
+
|
360 |
+
.paper-list a {
|
361 |
+
color: #4285f4;
|
362 |
+
text-decoration: none;
|
363 |
+
font-size: 13px;
|
364 |
+
display: block;
|
365 |
+
}
|
366 |
+
|
367 |
+
.paper-list a:hover {
|
368 |
+
text-decoration: underline;
|
369 |
+
}
|
370 |
+
|
371 |
+
.paper-type-label {
|
372 |
+
display: inline-block;
|
373 |
+
padding: 2px 6px;
|
374 |
+
margin-right: 5px;
|
375 |
+
border-radius: 3px;
|
376 |
+
font-size: 11px;
|
377 |
+
color: white;
|
378 |
+
}
|
379 |
+
|
380 |
+
.paper-type-main {
|
381 |
+
background-color: #4285f4;
|
382 |
+
}
|
383 |
+
|
384 |
+
.paper-type-reference {
|
385 |
+
background-color: #34a853;
|
386 |
+
}
|
387 |
+
|
388 |
+
.paper-type-citation {
|
389 |
+
background-color: #ea4335;
|
390 |
+
}
|
391 |
+
|
392 |
+
.paper-type-bridge {
|
393 |
+
background-color: #fbbc05;
|
394 |
+
}
|
395 |
+
|
396 |
+
/* Make the legend move up when details panel is open */
|
397 |
+
.paper-details-panel.active + .legend {
|
398 |
+
top: calc(80vh + 30px);
|
399 |
+
}
|
400 |
+
|
401 |
+
/* Selected node styling */
|
402 |
+
.node.selected circle,
|
403 |
+
.node.selected rect,
|
404 |
+
.node.selected path {
|
405 |
+
stroke: #000;
|
406 |
+
stroke-width: 2px;
|
407 |
+
}
|
408 |
+
|
409 |
+
/* Responsive design for smaller screens */
|
410 |
+
@media (max-width: 768px) {
|
411 |
+
.paper-details-panel {
|
412 |
+
width: 100%;
|
413 |
+
max-width: none;
|
414 |
+
top: auto;
|
415 |
+
bottom: 0;
|
416 |
+
right: 0;
|
417 |
+
max-height: 50vh;
|
418 |
+
border-radius: 5px 5px 0 0;
|
419 |
+
transform: translateY(100%);
|
420 |
+
}
|
421 |
+
|
422 |
+
.paper-details-panel.active {
|
423 |
+
transform: translateY(0);
|
424 |
+
}
|
425 |
+
|
426 |
+
.controls {
|
427 |
+
max-width: 250px;
|
428 |
+
}
|
429 |
+
|
430 |
+
.legend {
|
431 |
+
max-width: 200px;
|
432 |
+
}
|
433 |
+
}
|
434 |
+
/*
|
435 |
+
/* Add to gra.css */
|
436 |
+
/* .loading-section {
|
437 |
+
position: absolute;
|
438 |
+
top: 20px;
|
439 |
+
left: 20px;
|
440 |
+
background-color: white;
|
441 |
+
padding: 15px;
|
442 |
+
border-radius: 5px;
|
443 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
444 |
+
z-index: 100;
|
445 |
+
width: 300px;
|
446 |
+
} */
|
447 |
+
|
448 |
+
#statusMessage {
|
449 |
+
margin-bottom: 10px;
|
450 |
+
font-weight: bold;
|
451 |
+
}
|
452 |
+
|
453 |
+
.progress-container {
|
454 |
+
width: 100%;
|
455 |
+
height: 10px;
|
456 |
+
background-color: #f0f0f0;
|
457 |
+
border-radius: 5px;
|
458 |
+
overflow: hidden;
|
459 |
+
}
|
460 |
+
|
461 |
+
.progress-bar {
|
462 |
+
height: 100%;
|
463 |
+
width: 0%;
|
464 |
+
background-color: #4285f4;
|
465 |
+
transition: width 0.3s ease;
|
466 |
+
}
|
467 |
+
|
468 |
+
|
469 |
+
|
470 |
+
|
471 |
+
|
472 |
+
.time-navigation {
|
473 |
+
position: absolute;
|
474 |
+
bottom: 20px;
|
475 |
+
left: 50%;
|
476 |
+
transform: translateX(-50%);
|
477 |
+
background-color: var(--bg);
|
478 |
+
padding: 12px 15px;
|
479 |
+
border-radius: 5px;
|
480 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
481 |
+
display: flex;
|
482 |
+
align-items: center;
|
483 |
+
z-index: 100;
|
484 |
+
border: 8px solid #000; /* Add this line for a black border */
|
485 |
+
}
|
486 |
+
|
487 |
+
.time-navigation button {
|
488 |
+
margin: 0 10px;
|
489 |
+
padding: 8px 12px;
|
490 |
+
background-color: black;
|
491 |
+
color: white;
|
492 |
+
border: none;
|
493 |
+
border-radius: 4px;
|
494 |
+
cursor: pointer;
|
495 |
+
}
|
496 |
+
|
497 |
+
.time-navigation button:disabled {
|
498 |
+
background-color: black;
|
499 |
+
cursor: not-allowed;
|
500 |
+
}
|
501 |
+
|
502 |
+
.time-indicator {
|
503 |
+
font-weight: bold;
|
504 |
+
margin: 0 15px;
|
505 |
+
}
|
506 |
+
|
507 |
+
.time-slider-container {
|
508 |
+
display: flex;
|
509 |
+
flex-direction: column;
|
510 |
+
width: 400px;
|
511 |
+
}
|
512 |
+
|
513 |
+
.time-slider {
|
514 |
+
width: 100%;
|
515 |
+
margin: 5px 0;
|
516 |
+
}
|
517 |
+
|
518 |
+
.time-labels {
|
519 |
+
display: flex;
|
520 |
+
justify-content: space-between;
|
521 |
+
font-size: 12px;
|
522 |
+
color: #666;
|
523 |
+
}
|
524 |
+
|
525 |
+
.time-loader {
|
526 |
+
height: 3px;
|
527 |
+
background-color: #4285f4;
|
528 |
+
width: 0%;
|
529 |
+
transition: width 0.3s;
|
530 |
+
margin-top: 5px;
|
531 |
+
}
|
532 |
+
|
533 |
+
/* Animation for transitions */
|
534 |
+
.node.exiting {
|
535 |
+
opacity: 0;
|
536 |
+
transition: opacity 0.5s;
|
537 |
+
}
|
538 |
+
|
539 |
+
.node.entering {
|
540 |
+
opacity: 0;
|
541 |
+
animation: fadeIn 0.5s forwards;
|
542 |
+
animation-delay: 0.3s;
|
543 |
+
}
|
544 |
+
|
545 |
+
@keyframes fadeIn {
|
546 |
+
from { opacity: 0; }
|
547 |
+
to { opacity: 1; }
|
548 |
+
}
|
static/css/homestyle.css
ADDED
@@ -0,0 +1,1967 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
|
2 |
+
:root {
|
3 |
+
--primary-color: #4a90e2;
|
4 |
+
--secondary-color: #fcee0a;
|
5 |
+
--third-color: #6f42c1;
|
6 |
+
--text-color: #333;
|
7 |
+
--bg-color: #f5f7fa;
|
8 |
+
--white: #ffffff;
|
9 |
+
--forth-color: #00BFFF;
|
10 |
+
}
|
11 |
+
|
12 |
+
* {
|
13 |
+
margin: 0;
|
14 |
+
padding: 0;
|
15 |
+
box-sizing: border-box;
|
16 |
+
}
|
17 |
+
|
18 |
+
body {
|
19 |
+
font-family: 'Poppins', sans-serif;
|
20 |
+
line-height: 1.6;
|
21 |
+
background: var(--secondary-color);
|
22 |
+
color: var(--text-color);
|
23 |
+
}
|
24 |
+
|
25 |
+
header {
|
26 |
+
color: var(--secondary-color);
|
27 |
+
position: fixed;
|
28 |
+
width: 100%;
|
29 |
+
top: 0;
|
30 |
+
z-index: 1000;
|
31 |
+
display: flex;
|
32 |
+
justify-content: space-between;
|
33 |
+
align-items: center;
|
34 |
+
padding: 1rem 2%;
|
35 |
+
backdrop-filter: blur(10px); /* Optional: nice blur effect */
|
36 |
+
}
|
37 |
+
|
38 |
+
.header-content {
|
39 |
+
display: flex;
|
40 |
+
justify-content: space-between;
|
41 |
+
align-items: center;
|
42 |
+
width: 100%;
|
43 |
+
max-width: 1200px;
|
44 |
+
margin: 0 auto;
|
45 |
+
/* background-color: #fff; */
|
46 |
+
flex-wrap: wrap; /* Allow wrapping on smaller screens */
|
47 |
+
|
48 |
+
}
|
49 |
+
|
50 |
+
|
51 |
+
.logo-container {
|
52 |
+
display: flex;
|
53 |
+
align-items: center;
|
54 |
+
gap: 10px;
|
55 |
+
border-radius: 30px;
|
56 |
+
background-color: rgb(7, 7, 7);
|
57 |
+
padding: 10px 30px;
|
58 |
+
width: fit-content;
|
59 |
+
position: relative;
|
60 |
+
}
|
61 |
+
|
62 |
+
.logo-container::before {
|
63 |
+
content: "";
|
64 |
+
position: absolute;
|
65 |
+
top: 13px;
|
66 |
+
left: 13px;
|
67 |
+
right: 13px;
|
68 |
+
bottom: 13px;
|
69 |
+
border-radius: 26px;
|
70 |
+
border: 8px solid #faf8f8;
|
71 |
+
pointer-events: none;
|
72 |
+
}
|
73 |
+
|
74 |
+
|
75 |
+
#appname {
|
76 |
+
display: flex;
|
77 |
+
align-items: center;
|
78 |
+
font-size: clamp(1.2rem, 3vw, 2rem); /* Responsive font size */
|
79 |
+
white-space: nowrap;
|
80 |
+
margin-right: auto; /* Push other elements to the right */
|
81 |
+
color: #00bcd4;
|
82 |
+
|
83 |
+
}
|
84 |
+
|
85 |
+
/* Ensure header text remains visible */
|
86 |
+
.header-content h1#appname {
|
87 |
+
position: relative;
|
88 |
+
z-index: 2;
|
89 |
+
}
|
90 |
+
|
91 |
+
nav {
|
92 |
+
display: flex;
|
93 |
+
gap: clamp(1rem, 3vw, 3rem); /* Wider gaps between items */
|
94 |
+
align-items: center;
|
95 |
+
clip-path: polygon(0 0, 95% 0, 100% 100%, 5% 100%); /* More dramatic angle */
|
96 |
+
background-color: #0b0b0b; /* Olive color like in Image 2 */
|
97 |
+
padding: 0.4rem 2rem; /* Balanced padding */
|
98 |
+
height: 38px; /* Slimmer height */
|
99 |
+
width: fit-content; /* Only as wide as needed */
|
100 |
+
}
|
101 |
+
|
102 |
+
|
103 |
+
nav a:hover {
|
104 |
+
color: rgba(255, 255, 255, 0.8);
|
105 |
+
transform: translateY(-2px);
|
106 |
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
107 |
+
}
|
108 |
+
|
109 |
+
nav a {
|
110 |
+
color: white;
|
111 |
+
text-decoration: none;
|
112 |
+
font-weight: bold;
|
113 |
+
text-transform: uppercase; /* Match the all-caps style in Image 2 */
|
114 |
+
font-size: 0.85rem;
|
115 |
+
letter-spacing: 0.5px;
|
116 |
+
height: 100%;
|
117 |
+
display: flex;
|
118 |
+
align-items: center;
|
119 |
+
}
|
120 |
+
|
121 |
+
|
122 |
+
nav a::after {
|
123 |
+
content: '';
|
124 |
+
position: absolute;
|
125 |
+
bottom: 0;
|
126 |
+
left: 0;
|
127 |
+
width: 0;
|
128 |
+
height: 2px;
|
129 |
+
background: var(--white);
|
130 |
+
|
131 |
+
transition: width 0.3s ease;
|
132 |
+
}
|
133 |
+
|
134 |
+
nav a:hover::after {
|
135 |
+
width: 100%;
|
136 |
+
background-color: #00bcd4;
|
137 |
+
}
|
138 |
+
|
139 |
+
|
140 |
+
.sign-in-btn {
|
141 |
+
background: rgb(35, 204, 216);
|
142 |
+
color: white;
|
143 |
+
border: none;
|
144 |
+
padding: 0.2rem 1.5rem;
|
145 |
+
border-radius: 25px;
|
146 |
+
cursor: pointer;
|
147 |
+
transition: all 0.3s ease;
|
148 |
+
font-size: 0.95rem;
|
149 |
+
letter-spacing: 0.5px;
|
150 |
+
box-shadow: 0 4px 10px rgba(35, 204, 216, 0.3);
|
151 |
+
position: relative;
|
152 |
+
overflow: hidden;
|
153 |
+
text-transform: none;
|
154 |
+
bottom: 5px;
|
155 |
+
}
|
156 |
+
|
157 |
+
.sign-in-btn:hover {
|
158 |
+
background: rgb(28, 175, 185);
|
159 |
+
transform: translateY(-2px);
|
160 |
+
box-shadow: 0 6px 12px rgba(174, 35, 216, 0.4);
|
161 |
+
}
|
162 |
+
|
163 |
+
/* Active/click effect */
|
164 |
+
.sign-in-btn:active {
|
165 |
+
transform: translateY(1px); /* Push down slightly when clicked */
|
166 |
+
box-shadow: 0 2px 8px rgba(201, 35, 216, 0.4); /* Reduced shadow */
|
167 |
+
}
|
168 |
+
|
169 |
+
/* Optional: Add a subtle shine/gleam effect */
|
170 |
+
.sign-in-btn::before {
|
171 |
+
content: '';
|
172 |
+
position: absolute;
|
173 |
+
top: -50%;
|
174 |
+
left: -50%;
|
175 |
+
width: 200%;
|
176 |
+
height: 200%;
|
177 |
+
background: rgba(255, 255, 255, 0.1);
|
178 |
+
transform: rotate(45deg);
|
179 |
+
opacity: 0;
|
180 |
+
transition: opacity 0.5s;
|
181 |
+
}
|
182 |
+
|
183 |
+
.sign-in-btn:hover::before {
|
184 |
+
opacity: 1;
|
185 |
+
animation: shine 1.5s ease-out;
|
186 |
+
}
|
187 |
+
|
188 |
+
@keyframes shine {
|
189 |
+
0% {
|
190 |
+
transform: translateX(-100%) rotate(45deg);
|
191 |
+
}
|
192 |
+
100% {
|
193 |
+
transform: translateX(100%) rotate(45deg);
|
194 |
+
}
|
195 |
+
}
|
196 |
+
|
197 |
+
#signInBtn {
|
198 |
+
display: block;
|
199 |
+
}
|
200 |
+
|
201 |
+
|
202 |
+
|
203 |
+
|
204 |
+
/* Hero Section */
|
205 |
+
.hero {
|
206 |
+
position: relative;
|
207 |
+
padding: 8rem 5% 4rem;
|
208 |
+
text-align: center;
|
209 |
+
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-color));
|
210 |
+
color: var(--white);
|
211 |
+
overflow: hidden;
|
212 |
+
}
|
213 |
+
|
214 |
+
.hero h2 {
|
215 |
+
font-size: 2.5rem;
|
216 |
+
margin-bottom: 1rem;
|
217 |
+
font-family: 'Bungee Shade', cursive;
|
218 |
+
font-weight: 700;
|
219 |
+
color: #0b0b0b;
|
220 |
+
}
|
221 |
+
|
222 |
+
/* Lower text with paper effect */
|
223 |
+
.liquid-container {
|
224 |
+
position: relative;
|
225 |
+
height: 150px;
|
226 |
+
margin: 0 auto;
|
227 |
+
perspective: 1000px;
|
228 |
+
}
|
229 |
+
|
230 |
+
.liquid-word {
|
231 |
+
position: absolute;
|
232 |
+
width: 100%;
|
233 |
+
top: 0;
|
234 |
+
left: 0;
|
235 |
+
opacity: 0;
|
236 |
+
font-size: 0; /* Start with 0 size */
|
237 |
+
text-align: center;
|
238 |
+
transform-style: preserve-3d;
|
239 |
+
transition: opacity 0.5s ease;
|
240 |
+
}
|
241 |
+
|
242 |
+
.liquid-word.active {
|
243 |
+
opacity: 1;
|
244 |
+
}
|
245 |
+
|
246 |
+
.liquid-char {
|
247 |
+
display: inline-block;
|
248 |
+
font-size: 55px;
|
249 |
+
color: #00CED1; /* Base color */
|
250 |
+
-webkit-text-stroke: 4px #008B8B; /* Outline */
|
251 |
+
text-shadow:
|
252 |
+
-2px -2px 0 #fff,
|
253 |
+
2px -2px 0 #fff,
|
254 |
+
-2px 2px 0 #fff,
|
255 |
+
2px 2px 0 #fff,
|
256 |
+
0px 4px 0 #006666, /* Inner shadow */
|
257 |
+
4px 4px 10px rgba(0,0,0,0.3); /* Outer shadow */
|
258 |
+
transform: translateY(100px) scale(0);
|
259 |
+
opacity: 0;
|
260 |
+
position: relative;
|
261 |
+
}
|
262 |
+
|
263 |
+
/* Paper pieces/confetti */
|
264 |
+
.paper-piece {
|
265 |
+
position: absolute;
|
266 |
+
background: #fff;
|
267 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
268 |
+
opacity: 0;
|
269 |
+
z-index: -1;
|
270 |
+
transform-origin: center;
|
271 |
+
transition: all 0.3s ease;
|
272 |
+
}
|
273 |
+
|
274 |
+
/* Paper drop effect (instead of drip) */
|
275 |
+
.paper-drop {
|
276 |
+
position: absolute;
|
277 |
+
bottom: -20px;
|
278 |
+
left: 50%;
|
279 |
+
width: 15px;
|
280 |
+
height: 20px;
|
281 |
+
background: #fff;
|
282 |
+
transform: translateX(-50%) scale(0);
|
283 |
+
transform-origin: top center;
|
284 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
285 |
+
clip-path: polygon(0% 0%, 100% 0%, 100% 70%, 50% 100%, 0% 70%);
|
286 |
+
}
|
287 |
+
|
288 |
+
/* Paper splash container */
|
289 |
+
.splash-container {
|
290 |
+
position: absolute;
|
291 |
+
width: 100%;
|
292 |
+
height: 100%;
|
293 |
+
pointer-events: none;
|
294 |
+
z-index: -1;
|
295 |
+
}
|
296 |
+
|
297 |
+
/* Animations */
|
298 |
+
@keyframes paperDrop {
|
299 |
+
0% { transform: translateX(-50%) scale(0); }
|
300 |
+
40% { transform: translateX(-50%) scale(1.2); }
|
301 |
+
60% { transform: translateX(-50%) scale(0.8); }
|
302 |
+
80% { transform: translateX(-50%) scale(1.1); }
|
303 |
+
100% { transform: translateX(-50%) scale(1); }
|
304 |
+
}
|
305 |
+
|
306 |
+
@keyframes paperSplash {
|
307 |
+
0% { transform: scale(0) rotate(0deg); opacity: 0.9; }
|
308 |
+
50% { opacity: 0.7; }
|
309 |
+
100% { transform: scale(1.5) rotate(var(--rotation)); opacity: 0; }
|
310 |
+
}
|
311 |
+
|
312 |
+
@keyframes paperIn {
|
313 |
+
0% { transform: translateY(100px) scale(0) rotate(10deg); opacity: 0; }
|
314 |
+
40% { transform: translateY(-20px) scale(1.2) rotate(-5deg); opacity: 1; }
|
315 |
+
60% { transform: translateY(10px) scale(0.9) rotate(3deg); opacity: 1; }
|
316 |
+
80% { transform: translateY(-5px) scale(1.05) rotate(-2deg); opacity: 1; }
|
317 |
+
100% { transform: translateY(0) scale(1) rotate(0deg); opacity: 1; }
|
318 |
+
}
|
319 |
+
|
320 |
+
@keyframes paperOut {
|
321 |
+
0% { transform: translateY(0) scale(1) rotate(0deg); opacity: 1; }
|
322 |
+
20% { transform: translateY(-10px) scale(1.1) rotate(-5deg); opacity: 0.8; }
|
323 |
+
100% { transform: translateY(-100px) scale(0) rotate(10deg); opacity: 0; }
|
324 |
+
}
|
325 |
+
|
326 |
+
@keyframes wobble {
|
327 |
+
0%, 100% { transform: translateY(0) rotate(0deg); }
|
328 |
+
25% { transform: translateY(-5px) rotate(-2deg); }
|
329 |
+
75% { transform: translateY(3px) rotate(2deg); }
|
330 |
+
}
|
331 |
+
|
332 |
+
.title-char {
|
333 |
+
display: inline-block;
|
334 |
+
animation: wobble 4s ease-in-out infinite;
|
335 |
+
animation-delay: calc(var(--i) * 0.1s);
|
336 |
+
}
|
337 |
+
|
338 |
+
/* Search Container */
|
339 |
+
.search-container {
|
340 |
+
max-width: 600px;
|
341 |
+
margin: 2rem auto;
|
342 |
+
width: fit-content;
|
343 |
+
border-radius: 40px;
|
344 |
+
display: flex;
|
345 |
+
justify-content: center;
|
346 |
+
align-items: center;
|
347 |
+
gap: 1rem;
|
348 |
+
background: var(--white);
|
349 |
+
padding: 0.5rem;
|
350 |
+
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
351 |
+
overflow: hidden;
|
352 |
+
transition: all 0.3s ease;
|
353 |
+
position: relative;
|
354 |
+
}
|
355 |
+
|
356 |
+
.search-container .searchBox {
|
357 |
+
margin: 0;
|
358 |
+
}
|
359 |
+
|
360 |
+
.searchBox {
|
361 |
+
position: relative;
|
362 |
+
display: flex;
|
363 |
+
background: var(--secondary-color);
|
364 |
+
height: 60px;
|
365 |
+
border-radius: 40px;
|
366 |
+
padding: 10px;
|
367 |
+
margin: 0 auto;
|
368 |
+
width: fit-content;
|
369 |
+
z-index: 10;
|
370 |
+
}
|
371 |
+
|
372 |
+
.searchBox:hover > .searchInput {
|
373 |
+
width: 290px;
|
374 |
+
padding: 0 6px;
|
375 |
+
color: #0b0b0b;
|
376 |
+
font-weight: bold;
|
377 |
+
}
|
378 |
+
|
379 |
+
.searchBox:hover > .searchButton {
|
380 |
+
background: white;
|
381 |
+
color: #2f3640;
|
382 |
+
}
|
383 |
+
|
384 |
+
#searchbtn {
|
385 |
+
color: white;
|
386 |
+
width: 40px;
|
387 |
+
height: 40px;
|
388 |
+
border-radius: 50%;
|
389 |
+
background: rgb(13, 223, 220);
|
390 |
+
display: flex;
|
391 |
+
justify-content: center;
|
392 |
+
align-items: center;
|
393 |
+
transition: 0.4s;
|
394 |
+
cursor: pointer;
|
395 |
+
margin-top: -2px;
|
396 |
+
font-size: 14px;
|
397 |
+
|
398 |
+
}
|
399 |
+
|
400 |
+
.searchInput {
|
401 |
+
border: none;
|
402 |
+
background: none;
|
403 |
+
outline: none;
|
404 |
+
float: left;
|
405 |
+
padding: 0;
|
406 |
+
color: white;
|
407 |
+
font-size: 16px;
|
408 |
+
transition: 0.4s;
|
409 |
+
line-height: 40px;
|
410 |
+
width: 0px;
|
411 |
+
}
|
412 |
+
|
413 |
+
/* Year Filter Visibility */
|
414 |
+
.filter-container {
|
415 |
+
display: none; /* Hidden by default */
|
416 |
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
417 |
+
}
|
418 |
+
|
419 |
+
.searchBox:hover .filter-container {
|
420 |
+
display: flex; /* Visible on hover */
|
421 |
+
opacity: 1;
|
422 |
+
visibility: visible;
|
423 |
+
}
|
424 |
+
|
425 |
+
/* Base Styling for Year Filter */
|
426 |
+
.year-filter {
|
427 |
+
position: relative;
|
428 |
+
height: 40px;
|
429 |
+
padding: 0.6rem 1.5rem;
|
430 |
+
border: none;
|
431 |
+
background: #f5f5f5;
|
432 |
+
color: #333;
|
433 |
+
font-family: 'Poppins', sans-serif;
|
434 |
+
font-size: 14px;
|
435 |
+
border-radius: 40px;
|
436 |
+
outline: none;
|
437 |
+
cursor: pointer;
|
438 |
+
transition: all 0.3s ease;
|
439 |
+
appearance: none; /* Remove default arrow */
|
440 |
+
-webkit-appearance: none;
|
441 |
+
-moz-appearance: none;
|
442 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6"><path fill="%234a4a4a" d="M0 0h12L6 6z"/></svg>');
|
443 |
+
background-repeat: no-repeat;
|
444 |
+
background-position: right 1rem center;
|
445 |
+
}
|
446 |
+
|
447 |
+
.year-filter:hover,
|
448 |
+
.year-filter:focus {
|
449 |
+
background-color: #e0e0e0;
|
450 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
451 |
+
}
|
452 |
+
|
453 |
+
@media screen and (max-width: 620px) {
|
454 |
+
.searchBox:hover > .searchInput {
|
455 |
+
width: 150px;
|
456 |
+
padding: 0 6px;
|
457 |
+
}
|
458 |
+
}
|
459 |
+
/* Submit Button */
|
460 |
+
.search-container button {
|
461 |
+
background: var(--secondary-color);
|
462 |
+
color: var(--white);
|
463 |
+
border: none;
|
464 |
+
padding: 0.8rem 2rem;
|
465 |
+
border-radius: 5px;
|
466 |
+
cursor: pointer;
|
467 |
+
font-size: 1rem;
|
468 |
+
transition: all 0.3s ease;
|
469 |
+
position: relative;
|
470 |
+
overflow: hidden;
|
471 |
+
}
|
472 |
+
|
473 |
+
/* Hover effect for the submit button */
|
474 |
+
.search-container button::before {
|
475 |
+
content: '';
|
476 |
+
position: absolute;
|
477 |
+
top: 0;
|
478 |
+
left: -100%;
|
479 |
+
width: 100%;
|
480 |
+
height: 100%;
|
481 |
+
background: rgba(255, 255, 255, 0.2);
|
482 |
+
transition: left 0.4s ease;
|
483 |
+
}
|
484 |
+
|
485 |
+
.search-container button:hover::before {
|
486 |
+
left: 100%;
|
487 |
+
}
|
488 |
+
|
489 |
+
.search-container button:hover {
|
490 |
+
background: #5a37a8;
|
491 |
+
transform: scale(1.05);
|
492 |
+
}
|
493 |
+
|
494 |
+
/* Responsive Design */
|
495 |
+
@media (max-width: 768px) {
|
496 |
+
.search-container {
|
497 |
+
flex-direction: column;
|
498 |
+
gap: 0.5rem;
|
499 |
+
}
|
500 |
+
|
501 |
+
.filter-container {
|
502 |
+
display: none;
|
503 |
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
504 |
+
width: 100%;
|
505 |
+
margin: 0.5rem 0;
|
506 |
+
}
|
507 |
+
|
508 |
+
.year-filter {
|
509 |
+
width: 100%;
|
510 |
+
}
|
511 |
+
|
512 |
+
.search-container button {
|
513 |
+
width: 100%;
|
514 |
+
}
|
515 |
+
}
|
516 |
+
|
517 |
+
|
518 |
+
|
519 |
+
|
520 |
+
|
521 |
+
/* architecture Section
|
522 |
+
.architecture {
|
523 |
+
background: linear-gradient(135deg, var( --text-color), var( --text-color));
|
524 |
+
color: var(--white);
|
525 |
+
padding: 4rem 5%;
|
526 |
+
} */
|
527 |
+
|
528 |
+
.architecture {
|
529 |
+
background: linear-gradient(135deg, var(--text-color), var(--text-color));
|
530 |
+
color: var(--white);
|
531 |
+
padding: 4rem 5%;
|
532 |
+
border: 8px solid black;
|
533 |
+
border-radius: 10px; /* Adding some border radius for a nicer look */
|
534 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
535 |
+
position: relative;
|
536 |
+
top: 20px;
|
537 |
+
margin-left: 20px;
|
538 |
+
margin-right: 10px;
|
539 |
+
width: calc(100% - 40px); /* Accounting for the left and right margins */
|
540 |
+
}
|
541 |
+
|
542 |
+
|
543 |
+
|
544 |
+
|
545 |
+
|
546 |
+
#contacts {
|
547 |
+
padding-top: 40px;
|
548 |
+
margin-left: 20px;
|
549 |
+
margin-right: 20px;
|
550 |
+
position: relative;
|
551 |
+
|
552 |
+
}
|
553 |
+
#feedback {
|
554 |
+
margin-left: 20px;
|
555 |
+
margin-right: 20px;
|
556 |
+
position: relative;
|
557 |
+
|
558 |
+
}
|
559 |
+
/* Footer */
|
560 |
+
footer {
|
561 |
+
background: #2c3e50;
|
562 |
+
color: var(--white);
|
563 |
+
text-align: center;
|
564 |
+
padding: 2rem;
|
565 |
+
}
|
566 |
+
|
567 |
+
|
568 |
+
/* /* Profile Styling */
|
569 |
+
/* .profile-container {
|
570 |
+
display: flex;
|
571 |
+
align-items: center;
|
572 |
+
cursor: pointer;
|
573 |
+
position: relative;
|
574 |
+
margin-left: auto;
|
575 |
+
} */
|
576 |
+
|
577 |
+
.profile-pic {
|
578 |
+
width: 40px;
|
579 |
+
height: 40px;
|
580 |
+
border-radius: 50%;
|
581 |
+
margin-left: 10px;
|
582 |
+
}
|
583 |
+
|
584 |
+
.username {
|
585 |
+
color: white;
|
586 |
+
font-weight: bold;
|
587 |
+
}
|
588 |
+
|
589 |
+
|
590 |
+
.logout-btn {
|
591 |
+
background: red;
|
592 |
+
color: white;
|
593 |
+
padding: 5px 10px;
|
594 |
+
border: none;
|
595 |
+
cursor: pointer;
|
596 |
+
margin-top: 10px;
|
597 |
+
}
|
598 |
+
|
599 |
+
|
600 |
+
|
601 |
+
.orb {
|
602 |
+
position: absolute;
|
603 |
+
border-radius: 10px; /* Slightly rounded corners for a paper-like effect */
|
604 |
+
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); /* Gradient using theme colors */
|
605 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); /* Subtle shadow for depth */
|
606 |
+
animation: floatOrb 10s ease-in-out infinite;
|
607 |
+
z-index: 0;
|
608 |
+
opacity: 0.3;
|
609 |
+
}
|
610 |
+
|
611 |
+
.orb:nth-child(1) {
|
612 |
+
width: 200px;
|
613 |
+
height: 200px;
|
614 |
+
top: 30%;
|
615 |
+
left: 10%;
|
616 |
+
animation-delay: 0s;
|
617 |
+
background: var(--forth-color); /* Gradient for a slight texture */
|
618 |
+
}
|
619 |
+
|
620 |
+
.orb:nth-child(2) {
|
621 |
+
width: 150px;
|
622 |
+
height: 150px;
|
623 |
+
top: 60%;
|
624 |
+
right: 15%;
|
625 |
+
animation-delay: -2s;
|
626 |
+
background: var(--third-color); /* Slightly different gradient */
|
627 |
+
}
|
628 |
+
|
629 |
+
.orb:nth-child(3) {
|
630 |
+
width: 100px;
|
631 |
+
height: 100px;
|
632 |
+
top: 30%;
|
633 |
+
right: 25%;
|
634 |
+
animation-delay: -4s;
|
635 |
+
background: var(--secondary-color); /* Another variation of theme texture */
|
636 |
+
}
|
637 |
+
|
638 |
+
|
639 |
+
/* Orb Styles for Features Section */
|
640 |
+
.orb1 {
|
641 |
+
position: absolute;
|
642 |
+
border-radius: 10px; /* Slightly rounded corners for a paper-like effect */
|
643 |
+
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); /* Gradient using theme colors */
|
644 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06); /* Subtle shadow for depth */
|
645 |
+
animation: floatOrb 10s ease-in-out infinite;
|
646 |
+
z-index: 0;
|
647 |
+
opacity: 0.3;
|
648 |
+
}
|
649 |
+
|
650 |
+
.orb1:nth-child(1) {
|
651 |
+
width: 150px;
|
652 |
+
height: 150px;
|
653 |
+
top: 10%;
|
654 |
+
left: 30%;
|
655 |
+
animation-delay: 0s;
|
656 |
+
background: var(--third-color); /* Gradient for a slight texture */
|
657 |
+
}
|
658 |
+
|
659 |
+
.orb1:nth-child(2) {
|
660 |
+
width: 100px;
|
661 |
+
height: 100px;
|
662 |
+
top: 60%;
|
663 |
+
right: 15%;
|
664 |
+
animation-delay: -2s;
|
665 |
+
background: var(--forth-color); /* Slightly different gradient */
|
666 |
+
}
|
667 |
+
|
668 |
+
|
669 |
+
@keyframes floatOrb {
|
670 |
+
0%, 100% { transform: translate(0, 0); }
|
671 |
+
50% { transform: translate(-20px, -20px); }
|
672 |
+
}
|
673 |
+
|
674 |
+
@keyframes floatOrb {
|
675 |
+
0%, 100% { transform: translate(0, 0); }
|
676 |
+
50% { transform: translate(-20px, -20px); }
|
677 |
+
}
|
678 |
+
|
679 |
+
.hero-content {
|
680 |
+
position: relative; /* Ensure content is above orbs */
|
681 |
+
z-index: 1; /* Content should be above orbs */
|
682 |
+
}
|
683 |
+
|
684 |
+
|
685 |
+
.paper {
|
686 |
+
position: fixed;
|
687 |
+
pointer-events: none;
|
688 |
+
transform-origin: center center;
|
689 |
+
filter: blur(1px);
|
690 |
+
mix-blend-mode: screen;
|
691 |
+
background: white;
|
692 |
+
top: 0;
|
693 |
+
left: 0;
|
694 |
+
}
|
695 |
+
|
696 |
+
.glow-wrapper {
|
697 |
+
position: relative;
|
698 |
+
flex: 1;
|
699 |
+
}
|
700 |
+
|
701 |
+
.glow {
|
702 |
+
position: absolute;
|
703 |
+
top: -2px;
|
704 |
+
left: -2px;
|
705 |
+
right: -2px;
|
706 |
+
bottom: -2px;
|
707 |
+
background: linear-gradient(90deg,
|
708 |
+
var(--primary-color), var(--secondary-color), var(--third-color), var(--forth-color),
|
709 |
+
var(--primary-color), var(--secondary-color), var(--third-color));
|
710 |
+
border-radius: 0px; /* Make it square */
|
711 |
+
z-index: 0;
|
712 |
+
background-size: 400%;
|
713 |
+
animation: animate 20s linear infinite;
|
714 |
+
}
|
715 |
+
|
716 |
+
.glow::before {
|
717 |
+
content: '';
|
718 |
+
position: absolute;
|
719 |
+
top: 2px;
|
720 |
+
left: 2px;
|
721 |
+
right: 2px;
|
722 |
+
bottom: 2px;
|
723 |
+
background: #f0f0f0;
|
724 |
+
border-radius: 0px; /* Ensure it's square */
|
725 |
+
z-index: 1;
|
726 |
+
transition: background-color 0.3s ease;
|
727 |
+
}
|
728 |
+
|
729 |
+
|
730 |
+
|
731 |
+
/* .search-input {
|
732 |
+
position: relative;
|
733 |
+
z-index: 2;
|
734 |
+
width: 100%;
|
735 |
+
border: none;
|
736 |
+
padding: 0.8rem;
|
737 |
+
border-radius: 25px;
|
738 |
+
outline: none;
|
739 |
+
background: transparent;
|
740 |
+
} */
|
741 |
+
|
742 |
+
.glow-wrapper:focus-within .glow::before {
|
743 |
+
background: var(--white);
|
744 |
+
}
|
745 |
+
|
746 |
+
@keyframes animate {
|
747 |
+
0% { background-position: 0 0; }
|
748 |
+
50% { background-position: 400% 0; }
|
749 |
+
100% { background-position: 0 0; }
|
750 |
+
}
|
751 |
+
|
752 |
+
/* Remove the original box-shadow focus effect */
|
753 |
+
.search-container input:focus {
|
754 |
+
box-shadow: none;
|
755 |
+
}
|
756 |
+
|
757 |
+
|
758 |
+
.beam-container {
|
759 |
+
position: relative;
|
760 |
+
display: flex;
|
761 |
+
height: 500px;
|
762 |
+
width: 90vw;
|
763 |
+
max-width: 800px;
|
764 |
+
align-items: center;
|
765 |
+
justify-content: center;
|
766 |
+
overflow: hidden;
|
767 |
+
padding: 10px;
|
768 |
+
}
|
769 |
+
|
770 |
+
.content-wrapper {
|
771 |
+
display: flex;
|
772 |
+
width: 100%;
|
773 |
+
max-width: 32rem; /* equivalent to max-w-lg */
|
774 |
+
flex-direction: row;
|
775 |
+
align-items: center;
|
776 |
+
justify-content: space-between;
|
777 |
+
gap: 2.5rem; /* equivalent to gap-10 */
|
778 |
+
}
|
779 |
+
|
780 |
+
.left-column, .middle-column, .right-column {
|
781 |
+
display: flex;
|
782 |
+
flex-direction: column;
|
783 |
+
align-items: center;
|
784 |
+
}
|
785 |
+
|
786 |
+
.left-column {
|
787 |
+
gap: 0.5rem; /* equivalent to gap-2 */
|
788 |
+
}
|
789 |
+
|
790 |
+
.circle {
|
791 |
+
z-index: 10;
|
792 |
+
display: flex;
|
793 |
+
width: 3rem; /* size-12 */
|
794 |
+
height: 3rem;
|
795 |
+
align-items: center;
|
796 |
+
justify-content: center;
|
797 |
+
border-radius: 9999px;
|
798 |
+
border: 2px solid #e2e8f0;
|
799 |
+
background-color: white;
|
800 |
+
padding: 0.75rem;
|
801 |
+
box-shadow: 0 0 20px -12px rgba(0, 0, 0, 0.8);
|
802 |
+
}
|
803 |
+
|
804 |
+
.large-circle {
|
805 |
+
width: 4rem; /* size-16 */
|
806 |
+
height: 4rem;
|
807 |
+
}
|
808 |
+
|
809 |
+
.circle img {
|
810 |
+
max-width: 100%;
|
811 |
+
max-height: 100%;
|
812 |
+
}
|
813 |
+
|
814 |
+
/* SVG and beam styling */
|
815 |
+
.beams-svg {
|
816 |
+
position: absolute;
|
817 |
+
top: 0;
|
818 |
+
left: 0;
|
819 |
+
width: 100%;
|
820 |
+
height: 100%;
|
821 |
+
pointer-events: none;
|
822 |
+
}
|
823 |
+
|
824 |
+
.beam {
|
825 |
+
stroke: #e2e8f0;
|
826 |
+
stroke-width: 2;
|
827 |
+
stroke-dasharray: 3 3;
|
828 |
+
animation: dashOffset 5s linear infinite;
|
829 |
+
opacity: 0.7;
|
830 |
+
}
|
831 |
+
|
832 |
+
@keyframes dashOffset {
|
833 |
+
from {
|
834 |
+
stroke-dashoffset: 0;
|
835 |
+
}
|
836 |
+
to {
|
837 |
+
stroke-dashoffset: 100;
|
838 |
+
}
|
839 |
+
}
|
840 |
+
|
841 |
+
.beam-glow {
|
842 |
+
stroke: #60a5fa;
|
843 |
+
stroke-width: 4;
|
844 |
+
filter: blur(4px);
|
845 |
+
opacity: 0.5;
|
846 |
+
animation: pulse 2s ease-in-out infinite;
|
847 |
+
}
|
848 |
+
|
849 |
+
@keyframes pulse {
|
850 |
+
0%, 100% {
|
851 |
+
opacity: 0.5;
|
852 |
+
}
|
853 |
+
50% {
|
854 |
+
opacity: 0.2;
|
855 |
+
}
|
856 |
+
}
|
857 |
+
|
858 |
+
.container {
|
859 |
+
position: relative;
|
860 |
+
width: 800px;
|
861 |
+
height: 400px;
|
862 |
+
}
|
863 |
+
|
864 |
+
|
865 |
+
.center-box {
|
866 |
+
position: absolute;
|
867 |
+
left: 50%;
|
868 |
+
top: 50%;
|
869 |
+
transform: translate(-50%, -50%);
|
870 |
+
width: 80px;
|
871 |
+
height: 80px;
|
872 |
+
background: white; /* White background */
|
873 |
+
border-radius: 12px;
|
874 |
+
display: flex;
|
875 |
+
justify-content: center;
|
876 |
+
align-items: center;
|
877 |
+
z-index: 2;
|
878 |
+
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
|
879 |
+
}
|
880 |
+
|
881 |
+
.settings-icon-wrapper {
|
882 |
+
width: 40px;
|
883 |
+
height: 40px;
|
884 |
+
animation: rotate 8s linear infinite;
|
885 |
+
display: flex;
|
886 |
+
justify-content: center;
|
887 |
+
align-items: center;
|
888 |
+
}
|
889 |
+
|
890 |
+
.settings-icon-wrapper img {
|
891 |
+
width: 40px;
|
892 |
+
height: 40px;
|
893 |
+
}
|
894 |
+
|
895 |
+
|
896 |
+
@keyframes rotate {
|
897 |
+
from { transform: rotate(0deg); }
|
898 |
+
to { transform: rotate(360deg); }
|
899 |
+
}
|
900 |
+
|
901 |
+
.paper-container {
|
902 |
+
position: absolute;
|
903 |
+
display: flex;
|
904 |
+
align-items: center;
|
905 |
+
gap: 8px;
|
906 |
+
color: var(--white);
|
907 |
+
font-size: medium;
|
908 |
+
font-weight: bold;
|
909 |
+
transform: translateY(-50%);
|
910 |
+
z-index: 2;
|
911 |
+
}
|
912 |
+
|
913 |
+
.paper-icon {
|
914 |
+
width: 20px;
|
915 |
+
height: 24px;
|
916 |
+
position: relative;
|
917 |
+
background: white;
|
918 |
+
border-radius: 2px;
|
919 |
+
display: flex;
|
920 |
+
justify-content: center;
|
921 |
+
align-items: center;
|
922 |
+
font-size: 9px;
|
923 |
+
font-weight: bold;
|
924 |
+
color: #555;
|
925 |
+
}
|
926 |
+
|
927 |
+
.paper-icon:before {
|
928 |
+
content: '';
|
929 |
+
position: absolute;
|
930 |
+
top: 0;
|
931 |
+
right: 0;
|
932 |
+
border-width: 0 10px 10px 0;
|
933 |
+
border-style: solid;
|
934 |
+
border-color: #ddd #7f56d9;
|
935 |
+
}
|
936 |
+
|
937 |
+
.paths-container {
|
938 |
+
position: absolute;
|
939 |
+
top: 0;
|
940 |
+
left: 0;
|
941 |
+
width: 100%;
|
942 |
+
height: 100%;
|
943 |
+
z-index: 1;
|
944 |
+
}
|
945 |
+
|
946 |
+
.path {
|
947 |
+
fill: none;
|
948 |
+
stroke: rgba(255, 255, 255, 0.6);
|
949 |
+
stroke-width: 1;
|
950 |
+
stroke-opacity: 0; /* Make lines invisible initially */
|
951 |
+
stroke-dasharray: 0, 1000; /* Hide stroke */
|
952 |
+
transition: stroke-opacity 0.3s ease-in-out;
|
953 |
+
}
|
954 |
+
|
955 |
+
|
956 |
+
.moving-paper {
|
957 |
+
position: absolute;
|
958 |
+
width: 20px;
|
959 |
+
height: 24px;
|
960 |
+
background: white;
|
961 |
+
border-radius: 2px;
|
962 |
+
z-index: 2;
|
963 |
+
transform: translate(-50%, -50%);
|
964 |
+
display: flex;
|
965 |
+
justify-content: center;
|
966 |
+
align-items: center;
|
967 |
+
font-size: 9px;
|
968 |
+
font-weight: bold;
|
969 |
+
color: #555;
|
970 |
+
}
|
971 |
+
|
972 |
+
.moving-paper:before {
|
973 |
+
content: '';
|
974 |
+
position: absolute;
|
975 |
+
top: 0;
|
976 |
+
right: 0;
|
977 |
+
border-width: 0 10px 10px 0;
|
978 |
+
border-style: solid;
|
979 |
+
border-color: #ddd #7f56d9;
|
980 |
+
}
|
981 |
+
|
982 |
+
.moving-paper {
|
983 |
+
z-index: 1;
|
984 |
+
}
|
985 |
+
|
986 |
+
.moving-dot {
|
987 |
+
position: absolute;
|
988 |
+
width: 8px;
|
989 |
+
height: 8px;
|
990 |
+
background: #4a90e2;
|
991 |
+
border-radius: 50%;
|
992 |
+
z-index: 1;
|
993 |
+
transform: translate(-50%, -50%);
|
994 |
+
}
|
995 |
+
|
996 |
+
.output-box {
|
997 |
+
position: absolute;
|
998 |
+
width: 35px;
|
999 |
+
height: 35px;
|
1000 |
+
background: rgba(255, 255, 255, 0.05);
|
1001 |
+
border-radius: 8px;
|
1002 |
+
border: 0.5px solid rgba(255, 255, 255, 0.1);
|
1003 |
+
z-index: 2;
|
1004 |
+
right: 80px;
|
1005 |
+
transform: translateY(-50%);
|
1006 |
+
display: flex;
|
1007 |
+
justify-content: center;
|
1008 |
+
align-items: center;
|
1009 |
+
}
|
1010 |
+
|
1011 |
+
.output-box.highlight {
|
1012 |
+
box-shadow: 0 0 15px #4a90e2;
|
1013 |
+
background: rgba(255, 255, 255, 0.1);
|
1014 |
+
}
|
1015 |
+
|
1016 |
+
.output-logo {
|
1017 |
+
border-radius: 8px;
|
1018 |
+
width: 35px;
|
1019 |
+
height: 35px;
|
1020 |
+
object-fit: fill;
|
1021 |
+
}
|
1022 |
+
|
1023 |
+
.box-title {
|
1024 |
+
position: absolute;
|
1025 |
+
bottom: -25px;
|
1026 |
+
margin-left: -25px;
|
1027 |
+
font-size: medium;
|
1028 |
+
font-weight: bold;
|
1029 |
+
color: var(--white);
|
1030 |
+
white-space: nowrap;
|
1031 |
+
text-align: center;
|
1032 |
+
width: 100%;
|
1033 |
+
}
|
1034 |
+
|
1035 |
+
.output-box {
|
1036 |
+
display: none;
|
1037 |
+
}
|
1038 |
+
|
1039 |
+
.center-box {
|
1040 |
+
transition: transform 0.3s ease-in-out;
|
1041 |
+
}
|
1042 |
+
|
1043 |
+
.center-box.pulse {
|
1044 |
+
transform: translate(-50%, -50%) scale(1.1);
|
1045 |
+
}
|
1046 |
+
|
1047 |
+
.edit-img {
|
1048 |
+
width: 20px; /* Adjust size */
|
1049 |
+
height: 20px;
|
1050 |
+
cursor: pointer;
|
1051 |
+
transition: transform 0.2s ease-in-out;
|
1052 |
+
}
|
1053 |
+
|
1054 |
+
.edit-img:hover {
|
1055 |
+
transform: scale(1.1);
|
1056 |
+
}
|
1057 |
+
|
1058 |
+
|
1059 |
+
/* Add these styles to your CSS file */
|
1060 |
+
.filter-container {
|
1061 |
+
position: relative;
|
1062 |
+
margin: 0 0.5rem;
|
1063 |
+
}
|
1064 |
+
|
1065 |
+
.year-filter {
|
1066 |
+
height: 100%;
|
1067 |
+
padding: 0.8rem;
|
1068 |
+
border: none;
|
1069 |
+
background: #f0f0f0;
|
1070 |
+
color: var(--text-color);
|
1071 |
+
font-family: 'Poppins', sans-serif;
|
1072 |
+
outline: none;
|
1073 |
+
cursor: pointer;
|
1074 |
+
transition: background-color 0.3s ease;
|
1075 |
+
appearance: none; /* Remove default arrow */
|
1076 |
+
-webkit-appearance: none;
|
1077 |
+
-moz-appearance: none;
|
1078 |
+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6"><path fill="%234a4a4a" d="M0 0h12L6 6z"/></svg>');
|
1079 |
+
background-repeat: no-repeat;
|
1080 |
+
background-position: right 0.8rem center;
|
1081 |
+
padding-right: 2rem;
|
1082 |
+
min-width: 120px;
|
1083 |
+
}
|
1084 |
+
|
1085 |
+
.year-filter:hover, .year-filter:focus {
|
1086 |
+
background-color: #e0e0e0;
|
1087 |
+
}
|
1088 |
+
|
1089 |
+
/* Adjust search container for the new filter */
|
1090 |
+
.search-container {
|
1091 |
+
align-items: center;
|
1092 |
+
}
|
1093 |
+
|
1094 |
+
/* Make the search container responsive */
|
1095 |
+
.logo-image {
|
1096 |
+
height: auto;
|
1097 |
+
max-height: 40px; /* Adjust based on your desired size */
|
1098 |
+
width: auto;
|
1099 |
+
margin-right: 1rem; /* Add space between logo and text */
|
1100 |
+
transition: all 0.3s ease; /* Smooth scaling */
|
1101 |
+
}
|
1102 |
+
|
1103 |
+
/* Make logo responsive */
|
1104 |
+
@media (max-width: 768px) {
|
1105 |
+
.logo-image {
|
1106 |
+
max-height: 30px; /* Smaller on mobile devices */
|
1107 |
+
}
|
1108 |
+
}
|
1109 |
+
@media (max-width: 768px) {
|
1110 |
+
.search-container {
|
1111 |
+
flex-direction: column;
|
1112 |
+
gap: 0.5rem;
|
1113 |
+
}
|
1114 |
+
|
1115 |
+
.filter-container {
|
1116 |
+
width: 100%;
|
1117 |
+
margin: 0.5rem 0;
|
1118 |
+
}
|
1119 |
+
|
1120 |
+
.year-filter {
|
1121 |
+
width: 100%;
|
1122 |
+
}
|
1123 |
+
|
1124 |
+
.search-container button {
|
1125 |
+
width: 100%;
|
1126 |
+
}
|
1127 |
+
}
|
1128 |
+
|
1129 |
+
/* Search Results Styling */
|
1130 |
+
.search-results {
|
1131 |
+
padding: 2rem 5%;
|
1132 |
+
background: var(--bg-color);
|
1133 |
+
min-height: 300px;
|
1134 |
+
}
|
1135 |
+
|
1136 |
+
.search-results h2 {
|
1137 |
+
color: var(--secondary-color);
|
1138 |
+
margin-bottom: 1.5rem;
|
1139 |
+
text-align: center;
|
1140 |
+
}
|
1141 |
+
|
1142 |
+
.paper-card {
|
1143 |
+
background: var(--white);
|
1144 |
+
border-radius: 10px;
|
1145 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
1146 |
+
padding: 1.5rem;
|
1147 |
+
margin-bottom: 1.5rem;
|
1148 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
1149 |
+
}
|
1150 |
+
|
1151 |
+
.paper-card:hover {
|
1152 |
+
transform: translateY(-5px);
|
1153 |
+
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
1154 |
+
}
|
1155 |
+
|
1156 |
+
.paper-card h3 {
|
1157 |
+
color: var(--primary-color);
|
1158 |
+
margin-bottom: 0.5rem;
|
1159 |
+
}
|
1160 |
+
|
1161 |
+
.paper-authors {
|
1162 |
+
color: var(--text-color);
|
1163 |
+
font-style: italic;
|
1164 |
+
margin-bottom: 0.5rem;
|
1165 |
+
}
|
1166 |
+
|
1167 |
+
.paper-year {
|
1168 |
+
display: inline-block;
|
1169 |
+
background: var(--secondary-color);
|
1170 |
+
color: white;
|
1171 |
+
padding: 0.2rem 0.5rem;
|
1172 |
+
border-radius: 4px;
|
1173 |
+
font-size: 0.9rem;
|
1174 |
+
margin-bottom: 1rem;
|
1175 |
+
}
|
1176 |
+
|
1177 |
+
.paper-abstract {
|
1178 |
+
color: var(--text-color);
|
1179 |
+
margin-bottom: 1rem;
|
1180 |
+
line-height: 1.6;
|
1181 |
+
}
|
1182 |
+
|
1183 |
+
.view-paper-btn {
|
1184 |
+
background: var(--primary-color);
|
1185 |
+
color: white;
|
1186 |
+
border: none;
|
1187 |
+
padding: 0.5rem 1rem;
|
1188 |
+
border-radius: 4px;
|
1189 |
+
cursor: pointer;
|
1190 |
+
transition: background-color 0.3s ease;
|
1191 |
+
}
|
1192 |
+
|
1193 |
+
.view-paper-btn:hover {
|
1194 |
+
background: var(--secondary-color);
|
1195 |
+
}
|
1196 |
+
|
1197 |
+
#input-paper {
|
1198 |
+
margin-bottom: 20px;
|
1199 |
+
}
|
1200 |
+
|
1201 |
+
|
1202 |
+
|
1203 |
+
.container {
|
1204 |
+
position: relative;
|
1205 |
+
width: 800px;
|
1206 |
+
height: 400px;
|
1207 |
+
margin: 0 auto; /* Add this to center the container horizontally */
|
1208 |
+
display: flex;
|
1209 |
+
flex-direction: column;
|
1210 |
+
align-items: center; /* This will center child elements horizontally */
|
1211 |
+
}
|
1212 |
+
|
1213 |
+
|
1214 |
+
.architecture-description {
|
1215 |
+
max-width: 800px;
|
1216 |
+
margin: 0 auto 40px auto;
|
1217 |
+
padding: 25px;
|
1218 |
+
background: var(--text-color);
|
1219 |
+
border-radius: 10px;
|
1220 |
+
backdrop-filter: blur(5px);
|
1221 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
1222 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
1223 |
+
}
|
1224 |
+
|
1225 |
+
.architecture-description h3 {
|
1226 |
+
color: var(--secondary-color);
|
1227 |
+
font-size: 1.8rem;
|
1228 |
+
margin-bottom: 15px;
|
1229 |
+
text-shadow: 0 0 10px var(--third-color);
|
1230 |
+
font-family: 'Bungee Shade', cursive;
|
1231 |
+
text-align: center; /* Ensures the text inside the h3 is centered */
|
1232 |
+
}
|
1233 |
+
.architecture-description p {
|
1234 |
+
line-height: 1.7;
|
1235 |
+
margin-bottom: 15px;
|
1236 |
+
font-size: 1.05rem;
|
1237 |
+
text-transform: capitalize;
|
1238 |
+
font-style: italic;
|
1239 |
+
font-weight: 200;
|
1240 |
+
color:white;
|
1241 |
+
text-align: center;
|
1242 |
+
font-family: serif;
|
1243 |
+
}
|
1244 |
+
|
1245 |
+
.architecture-features {
|
1246 |
+
display: flex;
|
1247 |
+
justify-content: space-between;
|
1248 |
+
margin-top: 25px;
|
1249 |
+
flex-wrap: wrap;
|
1250 |
+
gap: 20px;
|
1251 |
+
}
|
1252 |
+
|
1253 |
+
.architecture-feature {
|
1254 |
+
flex: 1;
|
1255 |
+
min-width: 200px;
|
1256 |
+
background: rgba(111, 66, 193, 0.2);
|
1257 |
+
padding: 15px;
|
1258 |
+
border-radius: 8px;
|
1259 |
+
border-left: 3px solid var(--forth-color);
|
1260 |
+
}
|
1261 |
+
|
1262 |
+
.architecture-feature h4 {
|
1263 |
+
color: white;
|
1264 |
+
font-size: 1.2rem;
|
1265 |
+
margin-bottom: 10px;
|
1266 |
+
}
|
1267 |
+
|
1268 |
+
.architecture-feature p {
|
1269 |
+
color: rgba(255, 255, 255, 0.8);
|
1270 |
+
font-size: 0.95rem;
|
1271 |
+
}
|
1272 |
+
|
1273 |
+
|
1274 |
+
.feature-selection-container {
|
1275 |
+
display: flex;
|
1276 |
+
flex-direction: column;
|
1277 |
+
align-items: center;
|
1278 |
+
}
|
1279 |
+
|
1280 |
+
.feature-selection {
|
1281 |
+
position: relative;
|
1282 |
+
display: flex;
|
1283 |
+
justify-content: center;
|
1284 |
+
align-items: center;
|
1285 |
+
margin-top: 50px;
|
1286 |
+
}
|
1287 |
+
|
1288 |
+
.feature-selection .glass {
|
1289 |
+
position: relative;
|
1290 |
+
width: 180px;
|
1291 |
+
height: 200px;
|
1292 |
+
background: linear-gradient(var(--text-color), var(--text-color));
|
1293 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1294 |
+
box-shadow: 0 25px 25px rgba(0, 0, 0, 0.25);
|
1295 |
+
display: flex;
|
1296 |
+
justify-content: center;
|
1297 |
+
align-items: center;
|
1298 |
+
transition: 0.5s;
|
1299 |
+
border-radius: 10px;
|
1300 |
+
margin: 0 -45px;
|
1301 |
+
backdrop-filter: blur(10px);
|
1302 |
+
transform: rotate(calc(var(--r) * 1deg));
|
1303 |
+
}
|
1304 |
+
|
1305 |
+
.feature-selection:hover .glass {
|
1306 |
+
transform: rotate(0deg);
|
1307 |
+
margin: 0 10px;
|
1308 |
+
}
|
1309 |
+
|
1310 |
+
.feature-selection .glass::before {
|
1311 |
+
content: attr(data-text);
|
1312 |
+
position: absolute;
|
1313 |
+
bottom: 0;
|
1314 |
+
width: 100%;
|
1315 |
+
height: 60px;
|
1316 |
+
background: rgba(255, 255, 255, 0.05);
|
1317 |
+
display: flex;
|
1318 |
+
justify-content: center;
|
1319 |
+
align-items: center;
|
1320 |
+
color: #fff;
|
1321 |
+
}
|
1322 |
+
|
1323 |
+
|
1324 |
+
|
1325 |
+
.feature-selection .glass svg {
|
1326 |
+
font-size: 2.5em;
|
1327 |
+
fill: #fff;
|
1328 |
+
}
|
1329 |
+
|
1330 |
+
|
1331 |
+
.selection-message {
|
1332 |
+
text-align: center;
|
1333 |
+
margin-top: 30px;
|
1334 |
+
padding: 12px;
|
1335 |
+
background: var(--text-color);
|
1336 |
+
border-radius: 8px;
|
1337 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
1338 |
+
color: var(--secondary-color);
|
1339 |
+
font-weight: 500;
|
1340 |
+
opacity: 0;
|
1341 |
+
transform: translateY(20px);
|
1342 |
+
transition: all 0.4s ease;
|
1343 |
+
}
|
1344 |
+
|
1345 |
+
.selection-message.visible {
|
1346 |
+
opacity: 1;
|
1347 |
+
transform: translateY(0);
|
1348 |
+
}
|
1349 |
+
|
1350 |
+
.glass.selected {
|
1351 |
+
outline: 3px solid var(--secondary-color, #00bcd4);
|
1352 |
+
box-shadow: 0 0 15px rgba(0, 188, 212, 0.6);
|
1353 |
+
}
|
1354 |
+
|
1355 |
+
|
1356 |
+
.glass-icon {
|
1357 |
+
width: 60px; /* or whatever size fits */
|
1358 |
+
height: 60px;
|
1359 |
+
object-fit: contain;
|
1360 |
+
}
|
1361 |
+
|
1362 |
+
|
1363 |
+
|
1364 |
+
|
1365 |
+
|
1366 |
+
|
1367 |
+
/* Add this to your existing CSS file or in a <style> tag in the <head> */
|
1368 |
+
.loading-overlay {
|
1369 |
+
position: fixed;
|
1370 |
+
top: 0;
|
1371 |
+
left: 0;
|
1372 |
+
width: 100%;
|
1373 |
+
height: 100%;
|
1374 |
+
background-color: rgba(255, 255, 255, 0.7);
|
1375 |
+
backdrop-filter: blur(8px);
|
1376 |
+
display: flex;
|
1377 |
+
justify-content: center;
|
1378 |
+
align-items: center;
|
1379 |
+
z-index: 9999;
|
1380 |
+
opacity: 0;
|
1381 |
+
visibility: hidden;
|
1382 |
+
transition: opacity 0.3s, visibility 0.3s;
|
1383 |
+
}
|
1384 |
+
|
1385 |
+
.loading-overlay.active {
|
1386 |
+
opacity: 1;
|
1387 |
+
visibility: visible;
|
1388 |
+
}
|
1389 |
+
|
1390 |
+
.loading-container {
|
1391 |
+
position: relative;
|
1392 |
+
width: 300px;
|
1393 |
+
height: 500px;
|
1394 |
+
display: flex;
|
1395 |
+
flex-direction: column;
|
1396 |
+
align-items: center;
|
1397 |
+
justify-content: center;
|
1398 |
+
}
|
1399 |
+
|
1400 |
+
.cyclone {
|
1401 |
+
position: absolute;
|
1402 |
+
width: 200px;
|
1403 |
+
height: 400px;
|
1404 |
+
top: 50px;
|
1405 |
+
}
|
1406 |
+
|
1407 |
+
.paper {
|
1408 |
+
position: absolute;
|
1409 |
+
background-color: white;
|
1410 |
+
border: 1px solid #ddd;
|
1411 |
+
transform-origin: center;
|
1412 |
+
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1);
|
1413 |
+
z-index: 10;
|
1414 |
+
}
|
1415 |
+
|
1416 |
+
.loading-text {
|
1417 |
+
position: absolute;
|
1418 |
+
bottom: 30px;
|
1419 |
+
font-size: 24px;
|
1420 |
+
color: #555;
|
1421 |
+
font-weight: bold;
|
1422 |
+
}
|
1423 |
+
|
1424 |
+
.ground {
|
1425 |
+
position: absolute;
|
1426 |
+
bottom: 70px;
|
1427 |
+
width: 120px;
|
1428 |
+
height: 10px;
|
1429 |
+
background: radial-gradient(ellipse at center, #eee, transparent 70%);
|
1430 |
+
border-radius: 50%;
|
1431 |
+
opacity: 0.3;
|
1432 |
+
}
|
1433 |
+
|
1434 |
+
|
1435 |
+
.paper-alert {
|
1436 |
+
background: #fff;
|
1437 |
+
border-radius: 2px;
|
1438 |
+
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23),
|
1439 |
+
0 0 0 1px rgba(0,0,0,0.05);
|
1440 |
+
min-width: 320px;
|
1441 |
+
max-width: 400px;
|
1442 |
+
position: fixed;
|
1443 |
+
right: 16px;
|
1444 |
+
top: 16px;
|
1445 |
+
overflow: hidden;
|
1446 |
+
opacity: 0;
|
1447 |
+
pointer-events: none;
|
1448 |
+
z-index: 10000;
|
1449 |
+
transform-origin: top right;
|
1450 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
1451 |
+
}
|
1452 |
+
|
1453 |
+
/* Torn paper effect */
|
1454 |
+
.paper-alert::before {
|
1455 |
+
content: '';
|
1456 |
+
position: absolute;
|
1457 |
+
left: 0;
|
1458 |
+
bottom: -2px;
|
1459 |
+
width: 100%;
|
1460 |
+
height: 4px;
|
1461 |
+
background-color: #fff;
|
1462 |
+
background-image:
|
1463 |
+
radial-gradient(circle at 2px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
1464 |
+
radial-gradient(circle at 10px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
1465 |
+
radial-gradient(circle at 18px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
1466 |
+
radial-gradient(circle at 26px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
1467 |
+
radial-gradient(circle at 34px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
1468 |
+
radial-gradient(circle at 42px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px);
|
1469 |
+
background-size: 50px 10px;
|
1470 |
+
z-index: 2;
|
1471 |
+
}
|
1472 |
+
|
1473 |
+
/* Paper texture */
|
1474 |
+
.paper-alert::after {
|
1475 |
+
content: '';
|
1476 |
+
position: absolute;
|
1477 |
+
left: 0;
|
1478 |
+
top: 0;
|
1479 |
+
width: 100%;
|
1480 |
+
height: 100%;
|
1481 |
+
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeBlend mode='screen'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.08'/%3E%3C/svg%3E");
|
1482 |
+
pointer-events: none;
|
1483 |
+
z-index: 1;
|
1484 |
+
}
|
1485 |
+
|
1486 |
+
.paper-alert.showAlert {
|
1487 |
+
opacity: 1;
|
1488 |
+
pointer-events: auto;
|
1489 |
+
}
|
1490 |
+
|
1491 |
+
.paper-alert.show {
|
1492 |
+
animation: paper_enter 0.7s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
|
1493 |
+
}
|
1494 |
+
|
1495 |
+
@keyframes paper_enter {
|
1496 |
+
0% { transform: translateX(100%) rotate(5deg); }
|
1497 |
+
50% { transform: translateX(-10%) rotate(-2deg); }
|
1498 |
+
75% { transform: translateX(5%) rotate(1deg); }
|
1499 |
+
100% { transform: translateX(0) rotate(0); }
|
1500 |
+
}
|
1501 |
+
|
1502 |
+
.paper-alert.hide {
|
1503 |
+
animation: paper_exit 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
|
1504 |
+
}
|
1505 |
+
|
1506 |
+
@keyframes paper_exit {
|
1507 |
+
0% { transform: translateX(0) rotate(0); }
|
1508 |
+
30% { transform: translateX(5%) rotate(1deg); }
|
1509 |
+
100% { transform: translateX(110%) rotate(5deg); }
|
1510 |
+
}
|
1511 |
+
|
1512 |
+
.paper-alert-content {
|
1513 |
+
position: relative;
|
1514 |
+
padding: 18px 15px;
|
1515 |
+
z-index: 3;
|
1516 |
+
}
|
1517 |
+
|
1518 |
+
.paper-pin {
|
1519 |
+
position: absolute;
|
1520 |
+
width: 12px;
|
1521 |
+
height: 12px;
|
1522 |
+
background: #f44336;
|
1523 |
+
border-radius: 50%;
|
1524 |
+
top: 10px;
|
1525 |
+
left: 20px;
|
1526 |
+
box-shadow: inset 0 0 0 2px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.2);
|
1527 |
+
}
|
1528 |
+
|
1529 |
+
.paper-pin:after {
|
1530 |
+
content: '';
|
1531 |
+
position: absolute;
|
1532 |
+
width: 6px;
|
1533 |
+
height: 6px;
|
1534 |
+
background: rgba(255,255,255,0.4);
|
1535 |
+
border-radius: 50%;
|
1536 |
+
top: 2px;
|
1537 |
+
left: 2px;
|
1538 |
+
}
|
1539 |
+
|
1540 |
+
.paper-clip {
|
1541 |
+
position: absolute;
|
1542 |
+
top: -8px;
|
1543 |
+
right: 30px;
|
1544 |
+
width: 30px;
|
1545 |
+
height: 30px;
|
1546 |
+
border: 2px solid #5d9cec;
|
1547 |
+
border-radius: 0 0 0 10px;
|
1548 |
+
border-top: none;
|
1549 |
+
border-right: none;
|
1550 |
+
transform: rotate(45deg);
|
1551 |
+
z-index: 10;
|
1552 |
+
}
|
1553 |
+
|
1554 |
+
.paper-clip:before {
|
1555 |
+
content: '';
|
1556 |
+
position: absolute;
|
1557 |
+
width: 15px;
|
1558 |
+
height: 10px;
|
1559 |
+
border: 2px solid #5d9cec;
|
1560 |
+
border-radius: 0 5px 0 5px;
|
1561 |
+
border-top: none;
|
1562 |
+
border-right: none;
|
1563 |
+
top: 0;
|
1564 |
+
left: 0;
|
1565 |
+
transform: rotate(0deg);
|
1566 |
+
}
|
1567 |
+
|
1568 |
+
.alert-icon {
|
1569 |
+
display: flex;
|
1570 |
+
align-items: center;
|
1571 |
+
margin-bottom: 10px;
|
1572 |
+
}
|
1573 |
+
|
1574 |
+
.alert-icon i {
|
1575 |
+
font-size: 22px;
|
1576 |
+
color: #f44336;
|
1577 |
+
margin-right: 10px;
|
1578 |
+
}
|
1579 |
+
|
1580 |
+
.alert-title {
|
1581 |
+
font-weight: 600;
|
1582 |
+
font-size: 16px;
|
1583 |
+
color: #333;
|
1584 |
+
margin: 0;
|
1585 |
+
}
|
1586 |
+
|
1587 |
+
.paper-alert .msg {
|
1588 |
+
margin-top: 6px;
|
1589 |
+
margin-left: 32px;
|
1590 |
+
font-size: 14px;
|
1591 |
+
color: #555;
|
1592 |
+
line-height: 1.4;
|
1593 |
+
}
|
1594 |
+
|
1595 |
+
.paper-alert .close-btn {
|
1596 |
+
position: absolute;
|
1597 |
+
top: 10px;
|
1598 |
+
right: 10px;
|
1599 |
+
width: 24px;
|
1600 |
+
height: 24px;
|
1601 |
+
display: flex;
|
1602 |
+
align-items: center;
|
1603 |
+
justify-content: center;
|
1604 |
+
cursor: pointer;
|
1605 |
+
background: rgba(0,0,0,0.05);
|
1606 |
+
border-radius: 50%;
|
1607 |
+
color: #666;
|
1608 |
+
transition: all 0.2s;
|
1609 |
+
z-index: 5;
|
1610 |
+
}
|
1611 |
+
|
1612 |
+
.paper-alert .close-btn:hover {
|
1613 |
+
background: rgba(0,0,0,0.1);
|
1614 |
+
color: #333;
|
1615 |
+
transform: rotate(90deg);
|
1616 |
+
}
|
1617 |
+
|
1618 |
+
.alert-btn {
|
1619 |
+
position: fixed;
|
1620 |
+
top: 7rem;
|
1621 |
+
right: 2.5rem;
|
1622 |
+
z-index: 1201;
|
1623 |
+
font-size: 0.9rem;
|
1624 |
+
padding: 0.7rem 1.5rem;
|
1625 |
+
border-radius: 5px;
|
1626 |
+
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
1627 |
+
color: white;
|
1628 |
+
border: none;
|
1629 |
+
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
|
1630 |
+
cursor: pointer;
|
1631 |
+
font-family: 'Segoe UI', sans-serif;
|
1632 |
+
font-weight: 600;
|
1633 |
+
letter-spacing: 0.5px;
|
1634 |
+
transition: all 0.3s;
|
1635 |
+
}
|
1636 |
+
|
1637 |
+
.alert-btn:hover {
|
1638 |
+
transform: translateY(-3px) scale(1.03);
|
1639 |
+
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
|
1640 |
+
}
|
1641 |
+
|
1642 |
+
.paper-lines {
|
1643 |
+
position: absolute;
|
1644 |
+
left: 0;
|
1645 |
+
top: 0;
|
1646 |
+
width: 100%;
|
1647 |
+
height: 100%;
|
1648 |
+
background-image:
|
1649 |
+
linear-gradient(to bottom, transparent 0%, transparent 98%, #e0e0e0 99%, transparent 100%);
|
1650 |
+
background-size: 100% 24px;
|
1651 |
+
z-index: 0;
|
1652 |
+
pointer-events: none;
|
1653 |
+
}
|
1654 |
+
|
1655 |
+
.handwritten {
|
1656 |
+
font-family: 'Segoe Script', 'Bradley Hand', cursive;
|
1657 |
+
transform: rotate(-1deg);
|
1658 |
+
color: #2c3e50;
|
1659 |
+
}
|
1660 |
+
|
1661 |
+
|
1662 |
+
#nextBtn {
|
1663 |
+
background: var(--secondary-color);
|
1664 |
+
color: var(--white);
|
1665 |
+
border: none;
|
1666 |
+
padding: 0.8rem 2rem;
|
1667 |
+
border-radius: 15px;
|
1668 |
+
cursor: pointer;
|
1669 |
+
transition: background-color 0.3s ease;
|
1670 |
+
bottom: 7px;
|
1671 |
+
}
|
1672 |
+
|
1673 |
+
#nextBtn:hover {
|
1674 |
+
background: #5a37a8;
|
1675 |
+
}
|
1676 |
+
|
1677 |
+
.header-title {
|
1678 |
+
text-align: center;
|
1679 |
+
margin: 0 auto;
|
1680 |
+
width: 100%;
|
1681 |
+
position: relative;
|
1682 |
+
top: 50%;
|
1683 |
+
font-family: 'Bungee Shade', cursive;
|
1684 |
+
transform: translateY(35%);
|
1685 |
+
}
|
1686 |
+
|
1687 |
+
.interactive-book {
|
1688 |
+
max-height: fit-content;
|
1689 |
+
margin: 0;
|
1690 |
+
padding: 10px;
|
1691 |
+
background: linear-gradient(90deg, var(--secondary-color), var(--secondary-color));
|
1692 |
+
position: relative;
|
1693 |
+
}
|
1694 |
+
|
1695 |
+
.interactive-book::after {
|
1696 |
+
content: '';
|
1697 |
+
position: absolute;
|
1698 |
+
top: 10px;
|
1699 |
+
left: 20px;
|
1700 |
+
right: 20px;
|
1701 |
+
bottom: 20px;
|
1702 |
+
border: 8px solid black;
|
1703 |
+
pointer-events: none;
|
1704 |
+
border-radius: 12px;
|
1705 |
+
}
|
1706 |
+
|
1707 |
+
.carousel-container {
|
1708 |
+
position: relative;
|
1709 |
+
margin-top: -2px;
|
1710 |
+
width: 80%;
|
1711 |
+
height: 55vh;
|
1712 |
+
perspective: 1000px;
|
1713 |
+
display: flex;
|
1714 |
+
flex-direction: column;
|
1715 |
+
justify-content: space-between;
|
1716 |
+
align-items: center;
|
1717 |
+
margin-left: 10%;
|
1718 |
+
transform: scale(0.6);
|
1719 |
+
|
1720 |
+
}
|
1721 |
+
|
1722 |
+
.carousel {
|
1723 |
+
margin-top: 60px;
|
1724 |
+
position: absolute;
|
1725 |
+
width: 100%;
|
1726 |
+
height: 100%;
|
1727 |
+
transform-style: preserve-3d;
|
1728 |
+
transition: transform 1s ease;
|
1729 |
+
}
|
1730 |
+
|
1731 |
+
.slide {
|
1732 |
+
position: absolute;
|
1733 |
+
width: 60%;
|
1734 |
+
height: 80%;
|
1735 |
+
left: 20%;
|
1736 |
+
top: 10%;
|
1737 |
+
background: var(--text-color);
|
1738 |
+
border-radius: 10px;
|
1739 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
1740 |
+
display: flex;
|
1741 |
+
flex-direction: column;
|
1742 |
+
justify-content: center;
|
1743 |
+
align-items: center;
|
1744 |
+
padding: 20px;
|
1745 |
+
box-sizing: border-box;
|
1746 |
+
text-align: center;
|
1747 |
+
backface-visibility: hidden;
|
1748 |
+
transition: opacity 0.5s;
|
1749 |
+
}
|
1750 |
+
|
1751 |
+
|
1752 |
+
.slide video {
|
1753 |
+
width: 100%;
|
1754 |
+
height: 100%;
|
1755 |
+
object-fit: cover;
|
1756 |
+
border-radius: 3px;
|
1757 |
+
}
|
1758 |
+
|
1759 |
+
.slide h2 {
|
1760 |
+
font-size: 24px;
|
1761 |
+
margin-bottom: 15px;
|
1762 |
+
color: #333;
|
1763 |
+
}
|
1764 |
+
|
1765 |
+
.slide p {
|
1766 |
+
font-size: 16px;
|
1767 |
+
line-height: 1.6;
|
1768 |
+
color: #666;
|
1769 |
+
max-width: 80%;
|
1770 |
+
}
|
1771 |
+
|
1772 |
+
/* Refined Open Book Section */
|
1773 |
+
.book-container {
|
1774 |
+
position: relative;
|
1775 |
+
width: 300px;
|
1776 |
+
margin-top: 60px;
|
1777 |
+
height: 150px;
|
1778 |
+
margin-bottom: 30px;
|
1779 |
+
margin-left: 40%;
|
1780 |
+
}
|
1781 |
+
|
1782 |
+
.book {
|
1783 |
+
position: absolute;
|
1784 |
+
width: 100%;
|
1785 |
+
height: 100%;
|
1786 |
+
transform-style: preserve-3d;
|
1787 |
+
transform: rotateX(20deg);
|
1788 |
+
}
|
1789 |
+
|
1790 |
+
.book-center {
|
1791 |
+
position: absolute;
|
1792 |
+
width: 2px;
|
1793 |
+
height: 100%;
|
1794 |
+
background: #999;
|
1795 |
+
left: 50%;
|
1796 |
+
transform: translateX(20px) ;
|
1797 |
+
box-shadow: 0 0 5px rgba(0,0,0,0.3);
|
1798 |
+
z-index: 1;
|
1799 |
+
}
|
1800 |
+
|
1801 |
+
.book-left, .book-right {
|
1802 |
+
position: absolute;
|
1803 |
+
width: 50%;
|
1804 |
+
height: 100%;
|
1805 |
+
top: 0;
|
1806 |
+
box-sizing: border-box;
|
1807 |
+
background: #fff;
|
1808 |
+
border: 1px solid #ddd;
|
1809 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
1810 |
+
overflow: hidden;
|
1811 |
+
}
|
1812 |
+
|
1813 |
+
.book-left {
|
1814 |
+
left: 0;
|
1815 |
+
border-radius: 5px 0 0 5px;
|
1816 |
+
transform-origin: right center;
|
1817 |
+
z-index: 1;
|
1818 |
+
background: linear-gradient(to right, #f9f9f9, #fff);
|
1819 |
+
}
|
1820 |
+
|
1821 |
+
.book-right {
|
1822 |
+
right: 0;
|
1823 |
+
border-radius: 0 5px 5px 0;
|
1824 |
+
transform-origin: left center;
|
1825 |
+
z-index: 1;
|
1826 |
+
background: linear-gradient(to left, #f9f9f9, #fff);
|
1827 |
+
}
|
1828 |
+
|
1829 |
+
.page {
|
1830 |
+
position: absolute;
|
1831 |
+
width: 50%;
|
1832 |
+
height: 100%;
|
1833 |
+
top: 0;
|
1834 |
+
right: 0;
|
1835 |
+
background: #fff;
|
1836 |
+
border-left: 1px solid #eee;
|
1837 |
+
transform-origin: left center;
|
1838 |
+
transition: transform 0.6s ease, z-index 0s 0.3s;
|
1839 |
+
transform-style: preserve-3d;
|
1840 |
+
z-index: 1;
|
1841 |
+
border-radius: 0 5px 5px 0;
|
1842 |
+
}
|
1843 |
+
|
1844 |
+
.page-front, .page-back {
|
1845 |
+
position: absolute;
|
1846 |
+
width: 100%;
|
1847 |
+
height: 100%;
|
1848 |
+
top: 0;
|
1849 |
+
left: 0;
|
1850 |
+
backface-visibility: hidden;
|
1851 |
+
background: #fff;
|
1852 |
+
box-sizing: border-box;
|
1853 |
+
padding: 10px;
|
1854 |
+
background: linear-gradient(to left, #f9f9f9, #fff);
|
1855 |
+
}
|
1856 |
+
|
1857 |
+
.page-back {
|
1858 |
+
transform: rotateY(180deg);
|
1859 |
+
background: linear-gradient(to right, #f9f9f9, #fff);
|
1860 |
+
border-radius: 5px 0 0 5px;
|
1861 |
+
}
|
1862 |
+
|
1863 |
+
.page-fold {
|
1864 |
+
position: absolute;
|
1865 |
+
width: 30px;
|
1866 |
+
height: 100%;
|
1867 |
+
top: 0;
|
1868 |
+
left: 0;
|
1869 |
+
background: linear-gradient(to right, rgba(0,0,0,0.05), rgba(0,0,0,0));
|
1870 |
+
z-index: 2;
|
1871 |
+
}
|
1872 |
+
|
1873 |
+
.page-shadow {
|
1874 |
+
position: absolute;
|
1875 |
+
width: 100%;
|
1876 |
+
height: 100%;
|
1877 |
+
top: 0;
|
1878 |
+
left: 0;
|
1879 |
+
background: linear-gradient(to right, rgba(0,0,0,0.1), rgba(0,0,0,0));
|
1880 |
+
opacity: 0;
|
1881 |
+
transition: opacity 0.6s;
|
1882 |
+
pointer-events: none;
|
1883 |
+
}
|
1884 |
+
|
1885 |
+
.book-shadow {
|
1886 |
+
position: absolute;
|
1887 |
+
width: 100%;
|
1888 |
+
height: 20px;
|
1889 |
+
bottom: -20px;
|
1890 |
+
left: 0;
|
1891 |
+
background: radial-gradient(ellipse at center, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0) 70%);
|
1892 |
+
transform: rotateX(90deg);
|
1893 |
+
z-index: 0;
|
1894 |
+
}
|
1895 |
+
|
1896 |
+
.page-content {
|
1897 |
+
width: 100%;
|
1898 |
+
height: 100%;
|
1899 |
+
display: flex;
|
1900 |
+
justify-content: center;
|
1901 |
+
align-items: center;
|
1902 |
+
}
|
1903 |
+
|
1904 |
+
|
1905 |
+
.page-number {
|
1906 |
+
position: absolute;
|
1907 |
+
bottom: 10px;
|
1908 |
+
font-size: 12px;
|
1909 |
+
color: #999;
|
1910 |
+
}
|
1911 |
+
|
1912 |
+
.page-number-left {
|
1913 |
+
left: 10px;
|
1914 |
+
}
|
1915 |
+
|
1916 |
+
.page-number-right {
|
1917 |
+
right: 10px;
|
1918 |
+
}
|
1919 |
+
|
1920 |
+
.page-line {
|
1921 |
+
width: 80%;
|
1922 |
+
height: 1px;
|
1923 |
+
background: #eee;
|
1924 |
+
margin: 10px 0;
|
1925 |
+
}
|
1926 |
+
|
1927 |
+
.navigation {
|
1928 |
+
display: flex;
|
1929 |
+
gap: 8px;
|
1930 |
+
margin-top: 20px;
|
1931 |
+
justify-content: space-between;
|
1932 |
+
}
|
1933 |
+
|
1934 |
+
button {
|
1935 |
+
padding: 10px 20px;
|
1936 |
+
background: #3498db;
|
1937 |
+
color: white;
|
1938 |
+
border: none;
|
1939 |
+
margin-top: 10px;
|
1940 |
+
border-radius: 5px;
|
1941 |
+
cursor: pointer;
|
1942 |
+
transition: background 0.3s;
|
1943 |
+
font-size: 14px;
|
1944 |
+
}
|
1945 |
+
|
1946 |
+
button:hover {
|
1947 |
+
background: #2980b9;
|
1948 |
+
}
|
1949 |
+
|
1950 |
+
.pagination {
|
1951 |
+
display: flex;
|
1952 |
+
justify-content: center;
|
1953 |
+
gap: 10px;
|
1954 |
+
margin-top: 15px;
|
1955 |
+
}
|
1956 |
+
|
1957 |
+
.dot {
|
1958 |
+
width: 10px;
|
1959 |
+
height: 10px;
|
1960 |
+
background: #ccc;
|
1961 |
+
border-radius: 50%;
|
1962 |
+
cursor: pointer;
|
1963 |
+
}
|
1964 |
+
|
1965 |
+
.dot.active {
|
1966 |
+
background: #3498db;
|
1967 |
+
}
|
static/css/loginstyle.css
ADDED
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
2 |
+
|
3 |
+
* {
|
4 |
+
margin: 0;
|
5 |
+
padding: 0;
|
6 |
+
box-sizing: border-box;
|
7 |
+
font-family: 'Poppins', sans-serif;
|
8 |
+
}
|
9 |
+
|
10 |
+
:root {
|
11 |
+
--mainColor1: #4a90e2;
|
12 |
+
--mainColor2: #7f56d9;
|
13 |
+
--whiteColor: #ffffff;
|
14 |
+
--titleColor: #555555;
|
15 |
+
--labelColor: #333333;
|
16 |
+
--text-color: #333;
|
17 |
+
--secondary-color: #fcee0a;
|
18 |
+
--aqua: #47f5f2;
|
19 |
+
}
|
20 |
+
|
21 |
+
html {
|
22 |
+
font-size: 62.5%;
|
23 |
+
scroll-behavior: smooth;
|
24 |
+
}
|
25 |
+
|
26 |
+
body {
|
27 |
+
background: linear-gradient(to right, var(--secondary-color), var(--secondary-color));
|
28 |
+
font-weight: 400;
|
29 |
+
min-height: 100vh;
|
30 |
+
display: grid;
|
31 |
+
place-content: center;
|
32 |
+
overflow: hidden;
|
33 |
+
}
|
34 |
+
|
35 |
+
|
36 |
+
.paper-fall {
|
37 |
+
position: absolute;
|
38 |
+
width: 100%;
|
39 |
+
height: 100%;
|
40 |
+
overflow: hidden;
|
41 |
+
}
|
42 |
+
|
43 |
+
.paper {
|
44 |
+
position: absolute;
|
45 |
+
width: 50px;
|
46 |
+
height: 50px;
|
47 |
+
background-color: #fff;
|
48 |
+
border-radius: 2px;
|
49 |
+
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
50 |
+
animation: fall linear infinite;
|
51 |
+
}
|
52 |
+
|
53 |
+
@keyframes fall {
|
54 |
+
0% {
|
55 |
+
transform: translateY(-100%);
|
56 |
+
opacity: 0;
|
57 |
+
}
|
58 |
+
10% {
|
59 |
+
opacity: 1;
|
60 |
+
}
|
61 |
+
100% {
|
62 |
+
transform: translateY(100vh);
|
63 |
+
opacity: 0;
|
64 |
+
}
|
65 |
+
}
|
66 |
+
|
67 |
+
|
68 |
+
.wrapper {
|
69 |
+
position: relative;
|
70 |
+
width: 35rem;
|
71 |
+
height: 50rem;
|
72 |
+
}
|
73 |
+
|
74 |
+
@media(min-width: 540px) {
|
75 |
+
.wrapper {
|
76 |
+
width: 40rem;
|
77 |
+
}
|
78 |
+
}
|
79 |
+
|
80 |
+
.form-container {
|
81 |
+
position: absolute;
|
82 |
+
top: 0;
|
83 |
+
left: 0;
|
84 |
+
display: flex;
|
85 |
+
align-items: center;
|
86 |
+
justify-content: center;
|
87 |
+
width: 100%;
|
88 |
+
height: 100%;
|
89 |
+
background: var(--text-color);
|
90 |
+
border-radius: 0.5rem;
|
91 |
+
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2);
|
92 |
+
}
|
93 |
+
|
94 |
+
.form-container h2 {
|
95 |
+
font-size: 3rem;
|
96 |
+
text-align: center;
|
97 |
+
text-transform: capitalize;
|
98 |
+
color: var(--whiteColor);
|
99 |
+
}
|
100 |
+
|
101 |
+
.form-group {
|
102 |
+
position: relative;
|
103 |
+
width: 32rem;
|
104 |
+
margin: 3rem 0;
|
105 |
+
}
|
106 |
+
|
107 |
+
.form-group i,
|
108 |
+
.form-group label {
|
109 |
+
position: absolute;
|
110 |
+
top: 50%;
|
111 |
+
transform: translateY(-50%);
|
112 |
+
font-size: 1.6rem;
|
113 |
+
color: var(--labelColor);
|
114 |
+
padding: 0 0.5rem;
|
115 |
+
transition: all 0.5s ease;
|
116 |
+
pointer-events: none;
|
117 |
+
}
|
118 |
+
|
119 |
+
.form-group i {
|
120 |
+
left: 0.5rem;
|
121 |
+
}
|
122 |
+
|
123 |
+
.form-group label {
|
124 |
+
left: 2.5rem;
|
125 |
+
}
|
126 |
+
|
127 |
+
.form-group input {
|
128 |
+
width: 100%;
|
129 |
+
height: 4rem;
|
130 |
+
padding: 0 1rem;
|
131 |
+
border-radius: 0.5rem;
|
132 |
+
border: 0.1rem solid var(--whiteColor);
|
133 |
+
font-size: 1.6rem;
|
134 |
+
color: var(--labelColor);
|
135 |
+
background: white;
|
136 |
+
outline: none;
|
137 |
+
}
|
138 |
+
|
139 |
+
.form-group input:focus ~ label,
|
140 |
+
.form-group input:valid ~ label,
|
141 |
+
.form-group input:focus ~ i,
|
142 |
+
.form-group input:valid ~ i {
|
143 |
+
top: 0 !important;
|
144 |
+
font-size: 1.2rem !important;
|
145 |
+
background: var(--whiteColor);
|
146 |
+
}
|
147 |
+
|
148 |
+
.forgot-pass {
|
149 |
+
margin: -1.5rem 0 1.5rem;
|
150 |
+
text-align: center;
|
151 |
+
}
|
152 |
+
|
153 |
+
.forgot-pass a {
|
154 |
+
color: var(--whiteColor);
|
155 |
+
text-decoration: none;
|
156 |
+
font-size: 1.4rem;
|
157 |
+
transition: all 0.5s ease;
|
158 |
+
}
|
159 |
+
|
160 |
+
.forgot-pass a:hover {
|
161 |
+
color: var(--mainColor1);
|
162 |
+
}
|
163 |
+
|
164 |
+
|
165 |
+
.wrapper .form-container.forgot-password {
|
166 |
+
position: absolute;
|
167 |
+
top: 0;
|
168 |
+
left: 0;
|
169 |
+
width: 100%;
|
170 |
+
height: 100%;
|
171 |
+
background-color: var (-text-color);
|
172 |
+
border-radius: .5rem;
|
173 |
+
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.2);
|
174 |
+
transform: rotate(7deg);
|
175 |
+
}
|
176 |
+
|
177 |
+
/* Animation for Forgot Password */
|
178 |
+
.wrapper.animated-forgot .form-container.forgot-password {
|
179 |
+
animation: forgot-flip 1s ease-in-out forwards;
|
180 |
+
z-index: 3;
|
181 |
+
}
|
182 |
+
|
183 |
+
@keyframes forgot-flip {
|
184 |
+
0% {
|
185 |
+
transform: translateX(0);
|
186 |
+
}
|
187 |
+
50% {
|
188 |
+
transform: translateX(-50rem) scale(1.1);
|
189 |
+
}
|
190 |
+
100% {
|
191 |
+
transform: translateX(0) rotate(7deg) scale(1);
|
192 |
+
}
|
193 |
+
}
|
194 |
+
|
195 |
+
.wrapper.animated-forgot .form-container.sign-in,
|
196 |
+
.wrapper.animated-forgot .form-container.sign-up {
|
197 |
+
animation: rotatecard 0.7s ease-in-out forwards;
|
198 |
+
animation-delay: 0.3s;
|
199 |
+
}
|
200 |
+
|
201 |
+
|
202 |
+
|
203 |
+
|
204 |
+
.btn {
|
205 |
+
background: linear-gradient(to right, var(--aqua), var(--aqua));
|
206 |
+
color: var(--whiteColor);
|
207 |
+
width: 100%;
|
208 |
+
height: 4rem;
|
209 |
+
font-size: 1.6rem;
|
210 |
+
font-weight: 500;
|
211 |
+
border: none;
|
212 |
+
border-radius: 0.5rem;
|
213 |
+
cursor: pointer;
|
214 |
+
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.4);
|
215 |
+
}
|
216 |
+
|
217 |
+
.link {
|
218 |
+
text-align: center;
|
219 |
+
font-size: 1.4rem;
|
220 |
+
color: var(--labelColor);
|
221 |
+
margin: 2.5rem 0;
|
222 |
+
}
|
223 |
+
|
224 |
+
.link a {
|
225 |
+
color: var(--mainColor1);
|
226 |
+
text-decoration: none;
|
227 |
+
font-weight: 600;
|
228 |
+
transition: all 0.5s ease;
|
229 |
+
}
|
230 |
+
|
231 |
+
.link a:hover {
|
232 |
+
color: #da4453;
|
233 |
+
}
|
234 |
+
|
235 |
+
.sign-up {
|
236 |
+
transform: rotate(7deg);
|
237 |
+
}
|
238 |
+
|
239 |
+
.animated-signin .sign-in {
|
240 |
+
animation: signin-flip 1s ease-in-out forwards;
|
241 |
+
}
|
242 |
+
|
243 |
+
@keyframes signin-flip {
|
244 |
+
0% {
|
245 |
+
transform: translateX(0);
|
246 |
+
}
|
247 |
+
50% {
|
248 |
+
transform: translateX(-50rem) scale(1.1);
|
249 |
+
}
|
250 |
+
100% {
|
251 |
+
transform: translateX(0) rotate(7deg) scale(1);
|
252 |
+
}
|
253 |
+
}
|
254 |
+
|
255 |
+
.animated-signin .sign-up {
|
256 |
+
animation: rotatecard 0.7s ease-in-out forwards;
|
257 |
+
}
|
258 |
+
|
259 |
+
.animated-signup .sign-up {
|
260 |
+
animation: signup-flip 1s ease-in-out forwards;
|
261 |
+
}
|
262 |
+
|
263 |
+
@keyframes signup-flip {
|
264 |
+
0% {
|
265 |
+
transform: translateX(0);
|
266 |
+
z-index: 1;
|
267 |
+
}
|
268 |
+
50% {
|
269 |
+
transform: translateX(50rem) scale(1.1);
|
270 |
+
}
|
271 |
+
100% {
|
272 |
+
transform: translateX(0) rotate(7deg) scale(1);
|
273 |
+
}
|
274 |
+
}
|
275 |
+
|
276 |
+
.animated-signup .sign-in {
|
277 |
+
transform: rotate(7deg);
|
278 |
+
animation: rotatecard 0.7s ease-in-out forwards;
|
279 |
+
animation-delay: 0.3s;
|
280 |
+
}
|
281 |
+
|
282 |
+
@keyframes rotatecard {
|
283 |
+
0% {
|
284 |
+
transform: rotate(7deg);
|
285 |
+
}
|
286 |
+
100% {
|
287 |
+
transform: rotate(0);
|
288 |
+
z-index: 1;
|
289 |
+
}
|
290 |
+
}
|
291 |
+
|
292 |
+
/*
|
293 |
+
.success-animation {
|
294 |
+
position: fixed;
|
295 |
+
top: 50%;
|
296 |
+
left: 50%;
|
297 |
+
transform: translate(-50%, -50%);
|
298 |
+
z-index: 1000;
|
299 |
+
text-align: center;
|
300 |
+
}
|
301 |
+
|
302 |
+
.checkmark {
|
303 |
+
width: 80px;
|
304 |
+
height: 80px;
|
305 |
+
border-radius: 50%;
|
306 |
+
background: #7f56d9;
|
307 |
+
margin: 0 auto;
|
308 |
+
position: relative;
|
309 |
+
animation: scaleIn 0.3s ease-out;
|
310 |
+
}
|
311 |
+
|
312 |
+
.checkmark::after {
|
313 |
+
content: '';
|
314 |
+
position: absolute;
|
315 |
+
left: 25px;
|
316 |
+
top: 40px;
|
317 |
+
width: 25px;
|
318 |
+
height: 5px;
|
319 |
+
background: #fff;
|
320 |
+
transform: rotate(45deg);
|
321 |
+
animation: checkmark 0.3s ease-out;
|
322 |
+
}
|
323 |
+
|
324 |
+
.checkmark::before {
|
325 |
+
content: '';
|
326 |
+
position: absolute;
|
327 |
+
left: 15px;
|
328 |
+
top: 30px;
|
329 |
+
width: 40px;
|
330 |
+
height: 5px;
|
331 |
+
background: #fff;
|
332 |
+
transform: rotate(-45deg);
|
333 |
+
animation: checkmark 0.4s ease-out;
|
334 |
+
}
|
335 |
+
|
336 |
+
.success-text {
|
337 |
+
color: #7f56d9;
|
338 |
+
font-size: 2.4rem;
|
339 |
+
margin-top: 20px;
|
340 |
+
opacity: 0;
|
341 |
+
animation: fadeInUp 0.5s ease-out 0.3s forwards;
|
342 |
+
}
|
343 |
+
|
344 |
+
@keyframes scaleIn {
|
345 |
+
from {
|
346 |
+
transform: scale(0);
|
347 |
+
}
|
348 |
+
to {
|
349 |
+
transform: scale(1);
|
350 |
+
}
|
351 |
+
}
|
352 |
+
|
353 |
+
@keyframes checkmark {
|
354 |
+
from {
|
355 |
+
width: 0;
|
356 |
+
height: 0;
|
357 |
+
}
|
358 |
+
to {
|
359 |
+
width: 25px;
|
360 |
+
height: 5px;
|
361 |
+
}
|
362 |
+
}
|
363 |
+
|
364 |
+
@keyframes fadeInUp {
|
365 |
+
from {
|
366 |
+
opacity: 0;
|
367 |
+
transform: translateY(20px);
|
368 |
+
}
|
369 |
+
to {
|
370 |
+
opacity: 1;
|
371 |
+
transform: translateY(0);
|
372 |
+
}
|
373 |
+
}
|
374 |
+
|
375 |
+
.success-overlay {
|
376 |
+
position: fixed;
|
377 |
+
top: 0;
|
378 |
+
left: 0;
|
379 |
+
width: 100%;
|
380 |
+
height: 100%;
|
381 |
+
background: rgba(255, 255, 255, 0.9);
|
382 |
+
z-index: 999;
|
383 |
+
}
|
384 |
+
*/
|
385 |
+
|
386 |
+
|
387 |
+
|
388 |
+
/* Paper Animation Styles */
|
389 |
+
|
390 |
+
.page {
|
391 |
+
position: absolute;
|
392 |
+
width: 70px;
|
393 |
+
height: 90px;
|
394 |
+
background-color: var(--mainColor1);
|
395 |
+
border: 1px solid #ccc;
|
396 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
397 |
+
animation: scatter 3s ease-in-out forwards;
|
398 |
+
z-index: 1000;
|
399 |
+
}
|
400 |
+
|
401 |
+
.tear-left,
|
402 |
+
.tear-right {
|
403 |
+
position: absolute;
|
404 |
+
width: 75px;
|
405 |
+
height: 200px;
|
406 |
+
background-color: white;
|
407 |
+
border: 1px solid #ccc;
|
408 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
409 |
+
z-index: 1000;
|
410 |
+
}
|
411 |
+
|
412 |
+
.tear-left {
|
413 |
+
clip-path: polygon(0 0, 100% 0, 70% 100%, 0% 100%);
|
414 |
+
animation: tearLeft 2s ease-in-out forwards;
|
415 |
+
}
|
416 |
+
|
417 |
+
.tear-right {
|
418 |
+
clip-path: polygon(30% 0, 100% 0, 100% 100%, 0% 100%);
|
419 |
+
animation: tearRight 2s ease-in-out forwards;
|
420 |
+
}
|
421 |
+
|
422 |
+
@keyframes scatter {
|
423 |
+
0% {
|
424 |
+
transform: translate(0, 0) rotate(0deg);
|
425 |
+
opacity: 1;
|
426 |
+
}
|
427 |
+
100% {
|
428 |
+
transform: translate(var(--x), var(--y)) rotate(var(--rotate));
|
429 |
+
opacity: 0;
|
430 |
+
}
|
431 |
+
}
|
432 |
+
|
433 |
+
@keyframes tearLeft {
|
434 |
+
0% {
|
435 |
+
transform: translateX(0) rotate(0deg);
|
436 |
+
}
|
437 |
+
100% {
|
438 |
+
transform: translateX(-150px) rotate(-20deg);
|
439 |
+
}
|
440 |
+
}
|
441 |
+
|
442 |
+
@keyframes tearRight {
|
443 |
+
0% {
|
444 |
+
transform: translateX(0) rotate(0deg);
|
445 |
+
}
|
446 |
+
100% {
|
447 |
+
transform: translateX(150px) rotate(20deg);
|
448 |
+
}
|
449 |
+
}
|
450 |
+
|
451 |
+
.paper {
|
452 |
+
position: fixed;
|
453 |
+
pointer-events: none;
|
454 |
+
transform-origin: center center;
|
455 |
+
filter: blur(1px);
|
456 |
+
mix-blend-mode: screen;
|
457 |
+
background: white;
|
458 |
+
top: 0;
|
459 |
+
left: 0;
|
460 |
+
}
|
static/css/profilepage.css
ADDED
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--hprimary-color: #2c3e50;
|
3 |
+
--hsecondary-color: #1a2530;
|
4 |
+
--accent-color: #3498db;
|
5 |
+
--accent-hover: #2980b9;
|
6 |
+
--danger-color: #e74c3c;
|
7 |
+
--danger-hover: #c0392b;
|
8 |
+
--success-color: #2ecc71;
|
9 |
+
--warning-color: #f39c12;
|
10 |
+
--white: #ffffff;
|
11 |
+
--light-gray: #f5f5f5;
|
12 |
+
--gray: #95a5a6;
|
13 |
+
--yellow-color: #fcee0a;
|
14 |
+
--dark-gray: #7f8c8d;
|
15 |
+
--text-color: #2c3e50;
|
16 |
+
--card-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
17 |
+
--transition-speed: 0.3s;
|
18 |
+
}
|
19 |
+
|
20 |
+
/* Profile Trigger */
|
21 |
+
.profile-container {
|
22 |
+
position: relative;
|
23 |
+
margin-left: 20px;
|
24 |
+
}
|
25 |
+
|
26 |
+
.profile-trigger {
|
27 |
+
display: flex;
|
28 |
+
align-items: center;
|
29 |
+
cursor: pointer;
|
30 |
+
padding: 5px 10px;
|
31 |
+
border-radius: 50px;
|
32 |
+
background: rgba(255, 255, 255, 0.1);
|
33 |
+
transition: all var(--transition-speed) ease;
|
34 |
+
color: var(--white);
|
35 |
+
text-decoration: none;
|
36 |
+
}
|
37 |
+
|
38 |
+
.profile-trigger:hover {
|
39 |
+
background: rgba(255, 255, 255, 0.2);
|
40 |
+
}
|
41 |
+
|
42 |
+
.profile-trigger span {
|
43 |
+
margin-right: 8px;
|
44 |
+
font-weight: 500;
|
45 |
+
}
|
46 |
+
|
47 |
+
.profile-trigger i {
|
48 |
+
margin-left: 5px;
|
49 |
+
font-size: 12px;
|
50 |
+
transition: transform var(--transition-speed) ease;
|
51 |
+
}
|
52 |
+
|
53 |
+
.profile-trigger.active i {
|
54 |
+
transform: rotate(180deg);
|
55 |
+
}
|
56 |
+
|
57 |
+
.profile-pic {
|
58 |
+
width: 35px;
|
59 |
+
height: 35px;
|
60 |
+
border-radius: 50%;
|
61 |
+
object-fit: cover;
|
62 |
+
border: 2px solid var(--accent-color);
|
63 |
+
transition: all var(--transition-speed) ease;
|
64 |
+
margin-left: 8px;
|
65 |
+
}
|
66 |
+
|
67 |
+
.profile-pic:hover {
|
68 |
+
border-color: var(--accent-hover);
|
69 |
+
transform: scale(1.05);
|
70 |
+
}
|
71 |
+
|
72 |
+
/* Profile Dropdown */
|
73 |
+
.profile-overlay {
|
74 |
+
position: fixed;
|
75 |
+
top: 0;
|
76 |
+
left: 0;
|
77 |
+
width: 100%;
|
78 |
+
height: 100%;
|
79 |
+
background: rgba(0, 0, 0, 0.6);
|
80 |
+
backdrop-filter: blur(5px);
|
81 |
+
display: none;
|
82 |
+
z-index: 1000;
|
83 |
+
transition: opacity var(--transition-speed) ease;
|
84 |
+
}
|
85 |
+
|
86 |
+
/* Fix the transition variable */
|
87 |
+
.profile-dropdown {
|
88 |
+
position: fixed; /* Change from relative to fixed */
|
89 |
+
top: 0;
|
90 |
+
right: -450px;
|
91 |
+
width: 450px;
|
92 |
+
height: 100vh; /* Use viewport height instead */
|
93 |
+
background: linear-gradient(145deg, var(--hprimary-color), var(--hsecondary-color));
|
94 |
+
color: var(--white);
|
95 |
+
box-shadow: -5px 0 25px rgba(0, 0, 0, 0.3);
|
96 |
+
transition: right var(--transition-speed) ease-in-out; /* Fix variable name */
|
97 |
+
z-index: 1001;
|
98 |
+
overflow-y: auto;
|
99 |
+
scrollbar-width: thin;
|
100 |
+
scrollbar-color: var(--accent-color) var(--hsecondary-color);
|
101 |
+
}
|
102 |
+
|
103 |
+
/* Prevent body scrolling when profile is open */
|
104 |
+
body.profile-open {
|
105 |
+
overflow: hidden;
|
106 |
+
}
|
107 |
+
|
108 |
+
/* Show profile when active */
|
109 |
+
.profile-dropdown.active {
|
110 |
+
right: 0;
|
111 |
+
}
|
112 |
+
|
113 |
+
.profile-overlay.active {
|
114 |
+
display: block;
|
115 |
+
}
|
116 |
+
|
117 |
+
/* Profile Header */
|
118 |
+
.profile-header {
|
119 |
+
position: relative;
|
120 |
+
padding-bottom: 20px;
|
121 |
+
text-align: center;
|
122 |
+
}
|
123 |
+
|
124 |
+
.profile-cover-photo {
|
125 |
+
height: 120px;
|
126 |
+
background: linear-gradient(120deg,var(--yellow-color));
|
127 |
+
background-size: 300% 300%;
|
128 |
+
position: relative;
|
129 |
+
}
|
130 |
+
|
131 |
+
.profile-avatar-container {
|
132 |
+
position: relative;
|
133 |
+
margin-top: -50px;
|
134 |
+
display: inline-block;
|
135 |
+
}
|
136 |
+
|
137 |
+
#profileAvatar {
|
138 |
+
width: 100px;
|
139 |
+
height: 100px;
|
140 |
+
border-radius: 50%;
|
141 |
+
object-fit: cover;
|
142 |
+
border: 4px solid var(--white);
|
143 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
144 |
+
background: var(--white);
|
145 |
+
transition: all var(--transition-speed) ease;
|
146 |
+
}
|
147 |
+
|
148 |
+
.avatar-upload {
|
149 |
+
position: absolute;
|
150 |
+
bottom: 0;
|
151 |
+
right: 5px;
|
152 |
+
background: var(--accent-color);
|
153 |
+
width: 30px;
|
154 |
+
height: 30px;
|
155 |
+
border-radius: 50%;
|
156 |
+
display: flex;
|
157 |
+
align-items: center;
|
158 |
+
justify-content: center;
|
159 |
+
cursor: pointer;
|
160 |
+
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
161 |
+
transition: all var(--transition-speed) ease;
|
162 |
+
}
|
163 |
+
|
164 |
+
.avatar-upload:hover {
|
165 |
+
background: var(--accent-hover);
|
166 |
+
transform: scale(1.1);
|
167 |
+
}
|
168 |
+
|
169 |
+
.avatar-upload i {
|
170 |
+
color: var(--white);
|
171 |
+
font-size: 14px;
|
172 |
+
}
|
173 |
+
|
174 |
+
.avatar-upload input {
|
175 |
+
display: none;
|
176 |
+
}
|
177 |
+
|
178 |
+
#profileHeaderName {
|
179 |
+
margin: 15px 0 5px;
|
180 |
+
font-size: 22px;
|
181 |
+
font-weight: 600;
|
182 |
+
}
|
183 |
+
|
184 |
+
.membership-status {
|
185 |
+
display: inline-block;
|
186 |
+
padding: 4px 12px;
|
187 |
+
background: rgba(243, 156, 18, 0.2);
|
188 |
+
border-radius: 20px;
|
189 |
+
font-size: 14px;
|
190 |
+
margin: 0;
|
191 |
+
color: var(--warning-color);
|
192 |
+
}
|
193 |
+
|
194 |
+
.membership-status i {
|
195 |
+
margin-right: 5px;
|
196 |
+
}
|
197 |
+
|
198 |
+
/* Tabs */
|
199 |
+
.profile-tabs {
|
200 |
+
display: flex;
|
201 |
+
background: rgba(0, 0, 0, 0.2);
|
202 |
+
margin: 20px 0;
|
203 |
+
border-radius: 8px;
|
204 |
+
padding: 5px;
|
205 |
+
}
|
206 |
+
|
207 |
+
.tab-btn {
|
208 |
+
flex: 1;
|
209 |
+
background: transparent;
|
210 |
+
border: none;
|
211 |
+
padding: 12px;
|
212 |
+
color: var(--white);
|
213 |
+
font-weight: 500;
|
214 |
+
font-size: 14px;
|
215 |
+
cursor: pointer;
|
216 |
+
border-radius: 5px;
|
217 |
+
transition: all var(--transition-speed) ease;
|
218 |
+
}
|
219 |
+
|
220 |
+
.tab-btn:hover {
|
221 |
+
background: rgba(255, 255, 255, 0.1);
|
222 |
+
}
|
223 |
+
|
224 |
+
.tab-btn.active {
|
225 |
+
background: var(--accent-color);
|
226 |
+
color: var(--white);
|
227 |
+
}
|
228 |
+
|
229 |
+
.tab-content {
|
230 |
+
display: none;
|
231 |
+
padding: 0 20px;
|
232 |
+
}
|
233 |
+
|
234 |
+
.tab-content.active {
|
235 |
+
display: block;
|
236 |
+
animation: fadeIn 0.5s ease;
|
237 |
+
}
|
238 |
+
|
239 |
+
@keyframes fadeIn {
|
240 |
+
from { opacity: 0; transform: translateY(10px); }
|
241 |
+
to { opacity: 1; transform: translateY(0); }
|
242 |
+
}
|
243 |
+
|
244 |
+
/* Profile Fields */
|
245 |
+
.profile-field {
|
246 |
+
position: relative;
|
247 |
+
background: rgba(255, 255, 255, 0.1);
|
248 |
+
padding: 15px 20px;
|
249 |
+
border-radius: 12px;
|
250 |
+
margin-bottom: 20px;
|
251 |
+
transition: all var(--transition-speed) ease;
|
252 |
+
}
|
253 |
+
|
254 |
+
.profile-field:hover {
|
255 |
+
background: rgba(255, 255, 255, 0.15);
|
256 |
+
transform: translateY(-2px);
|
257 |
+
}
|
258 |
+
|
259 |
+
.profile-field.active {
|
260 |
+
background: rgba(255, 255, 255, 0.2);
|
261 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
262 |
+
}
|
263 |
+
|
264 |
+
.profile-field label {
|
265 |
+
display: block;
|
266 |
+
margin-bottom: 8px;
|
267 |
+
font-weight: 500;
|
268 |
+
font-size: 14px;
|
269 |
+
color: var(--light-gray);
|
270 |
+
}
|
271 |
+
|
272 |
+
.profile-field label i {
|
273 |
+
margin-right: 8px;
|
274 |
+
color: var(--accent-color);
|
275 |
+
}
|
276 |
+
|
277 |
+
.profile-field input {
|
278 |
+
width: 100%;
|
279 |
+
padding: 12px 15px;
|
280 |
+
background: rgba(0, 0, 0, 0.2);
|
281 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
282 |
+
border-radius: 8px;
|
283 |
+
color: var(--white);
|
284 |
+
font-size: 15px;
|
285 |
+
transition: all var(--transition-speed) ease;
|
286 |
+
font-family: inherit;
|
287 |
+
}
|
288 |
+
|
289 |
+
.profile-field input:focus {
|
290 |
+
background: rgba(0, 0, 0, 0.3);
|
291 |
+
border-color: var(--accent-color);
|
292 |
+
outline: none;
|
293 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
|
294 |
+
}
|
295 |
+
|
296 |
+
.profile-field input:disabled {
|
297 |
+
opacity: 0.8;
|
298 |
+
cursor: not-allowed;
|
299 |
+
}
|
300 |
+
|
301 |
+
.edit-icon {
|
302 |
+
position: absolute;
|
303 |
+
top: 15px;
|
304 |
+
right: 15px;
|
305 |
+
width: 30px;
|
306 |
+
height: 30px;
|
307 |
+
background: rgba(255, 255, 255, 0.15);
|
308 |
+
border-radius: 50%;
|
309 |
+
display: flex;
|
310 |
+
align-items: center;
|
311 |
+
justify-content: center;
|
312 |
+
cursor: pointer;
|
313 |
+
transition: all var(--transition-speed) ease;
|
314 |
+
}
|
315 |
+
|
316 |
+
.edit-icon:hover {
|
317 |
+
background: var(--accent-color);
|
318 |
+
transform: rotate(15deg);
|
319 |
+
}
|
320 |
+
|
321 |
+
.edit-icon i {
|
322 |
+
color: var(--white);
|
323 |
+
font-size: 12px;
|
324 |
+
}
|
325 |
+
|
326 |
+
.verification-badge {
|
327 |
+
position: absolute;
|
328 |
+
top: 15px;
|
329 |
+
right: 15px;
|
330 |
+
padding: 4px 10px;
|
331 |
+
background: rgba(46, 204, 113, 0.2);
|
332 |
+
color: var(--success-color);
|
333 |
+
border-radius: 20px;
|
334 |
+
font-size: 12px;
|
335 |
+
font-weight: 500;
|
336 |
+
}
|
337 |
+
|
338 |
+
.verification-badge i {
|
339 |
+
margin-right: 4px;
|
340 |
+
}
|
341 |
+
|
342 |
+
/* Toggle Switches */
|
343 |
+
.toggle-field {
|
344 |
+
display: flex;
|
345 |
+
justify-content: space-between;
|
346 |
+
align-items: center;
|
347 |
+
padding: 15px 20px;
|
348 |
+
background: rgba(255, 255, 255, 0.1);
|
349 |
+
border-radius: 12px;
|
350 |
+
margin-bottom: 15px;
|
351 |
+
transition: all var(--transition-speed) ease;
|
352 |
+
}
|
353 |
+
|
354 |
+
.toggle-field:hover {
|
355 |
+
background: rgba(255, 255, 255, 0.15);
|
356 |
+
transform: translateY(-2px);
|
357 |
+
}
|
358 |
+
|
359 |
+
.toggle-field span {
|
360 |
+
font-weight: 500;
|
361 |
+
}
|
362 |
+
|
363 |
+
.toggle-field span i {
|
364 |
+
margin-right: 8px;
|
365 |
+
color: var(--accent-color);
|
366 |
+
}
|
367 |
+
|
368 |
+
.switch {
|
369 |
+
position: relative;
|
370 |
+
display: inline-block;
|
371 |
+
width: 50px;
|
372 |
+
height: 26px;
|
373 |
+
}
|
374 |
+
|
375 |
+
.switch input {
|
376 |
+
opacity: 0;
|
377 |
+
width: 0;
|
378 |
+
height: 0;
|
379 |
+
}
|
380 |
+
|
381 |
+
.slider {
|
382 |
+
position: absolute;
|
383 |
+
cursor: pointer;
|
384 |
+
top: 0;
|
385 |
+
left: 0;
|
386 |
+
right: 0;
|
387 |
+
bottom: 0;
|
388 |
+
background-color: rgba(0, 0, 0, 0.3);
|
389 |
+
transition: .4s;
|
390 |
+
border-radius: 34px;
|
391 |
+
}
|
392 |
+
|
393 |
+
.slider:before {
|
394 |
+
position: absolute;
|
395 |
+
content: "";
|
396 |
+
height: 18px;
|
397 |
+
width: 18px;
|
398 |
+
left: 4px;
|
399 |
+
bottom: 4px;
|
400 |
+
background-color: white;
|
401 |
+
transition: .4s;
|
402 |
+
border-radius: 50%;
|
403 |
+
}
|
404 |
+
|
405 |
+
input:checked + .slider {
|
406 |
+
background-color: var(--accent-color);
|
407 |
+
}
|
408 |
+
|
409 |
+
input:focus + .slider {
|
410 |
+
box-shadow: 0 0 1px var(--accent-color);
|
411 |
+
}
|
412 |
+
|
413 |
+
input:checked + .slider:before {
|
414 |
+
transform: translateX(24px);
|
415 |
+
}
|
416 |
+
|
417 |
+
/* Select Field */
|
418 |
+
.select-field {
|
419 |
+
padding: 15px 20px;
|
420 |
+
background: rgba(255, 255, 255, 0.1);
|
421 |
+
border-radius: 12px;
|
422 |
+
margin-bottom: 15px;
|
423 |
+
}
|
424 |
+
|
425 |
+
.select-field label {
|
426 |
+
display: block;
|
427 |
+
margin-bottom: 10px;
|
428 |
+
font-weight: 500;
|
429 |
+
}
|
430 |
+
|
431 |
+
.select-field select {
|
432 |
+
width: 100%;
|
433 |
+
padding: 12px;
|
434 |
+
background: rgba(0, 0, 0, 0.2);
|
435 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
436 |
+
border-radius: 8px;
|
437 |
+
color: var(--white);
|
438 |
+
appearance: none;
|
439 |
+
background-image: url('data:image/svg+xml;utf8,<svg fill="white" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
|
440 |
+
background-repeat: no-repeat;
|
441 |
+
background-position: right 10px top 50%;
|
442 |
+
}
|
443 |
+
|
444 |
+
/* Activity Tab */
|
445 |
+
.activity-summary {
|
446 |
+
display: flex;
|
447 |
+
justify-content: space-between;
|
448 |
+
margin-bottom: 25px;
|
449 |
+
}
|
450 |
+
|
451 |
+
.activity-stat {
|
452 |
+
flex: 1;
|
453 |
+
text-align: center;
|
454 |
+
padding: 15px;
|
455 |
+
background: rgba(255, 255, 255, 0.1);
|
456 |
+
border-radius: 12px;
|
457 |
+
margin: 0 5px;
|
458 |
+
transition: all var(--transition-speed) ease;
|
459 |
+
}
|
460 |
+
|
461 |
+
.activity-stat:hover {
|
462 |
+
background: rgba(255, 255, 255, 0.2);
|
463 |
+
transform: translateY(-5px);
|
464 |
+
}
|
465 |
+
|
466 |
+
.activity-stat i {
|
467 |
+
font-size: 24px;
|
468 |
+
color: var(--accent-color);
|
469 |
+
margin-bottom: 8px;
|
470 |
+
}
|
471 |
+
|
472 |
+
.stat-value {
|
473 |
+
display: block;
|
474 |
+
font-size: 24px;
|
475 |
+
font-weight: 700;
|
476 |
+
margin-bottom: 5px;
|
477 |
+
}
|
478 |
+
|
479 |
+
.stat-label {
|
480 |
+
display: block;
|
481 |
+
font-size: 12px;
|
482 |
+
color: var(--gray);
|
483 |
+
text-transform: uppercase;
|
484 |
+
letter-spacing: 1px;
|
485 |
+
}
|
486 |
+
|
487 |
+
.section-title {
|
488 |
+
font-size: 18px;
|
489 |
+
margin: 20px 0 15px;
|
490 |
+
padding-bottom: 10px;
|
491 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
492 |
+
}
|
493 |
+
|
494 |
+
/* Timeline */
|
495 |
+
.timeline {
|
496 |
+
position: relative;
|
497 |
+
padding-left: 30px;
|
498 |
+
margin-bottom: 20px;
|
499 |
+
}
|
500 |
+
|
501 |
+
.timeline:before {
|
502 |
+
content: '';
|
503 |
+
position: absolute;
|
504 |
+
left: 7.5px;
|
505 |
+
top: 5px;
|
506 |
+
height: calc(100% - 10px);
|
507 |
+
width: 2px;
|
508 |
+
background: rgba(255, 255, 255, 0.2);
|
509 |
+
}
|
510 |
+
|
511 |
+
.timeline-item {
|
512 |
+
position: relative;
|
513 |
+
padding-bottom: 20px;
|
514 |
+
}
|
515 |
+
|
516 |
+
.timeline-dot {
|
517 |
+
position: absolute;
|
518 |
+
left: -30px;
|
519 |
+
top: 5px;
|
520 |
+
width: 15px;
|
521 |
+
height: 15px;
|
522 |
+
background: var(--accent-color);
|
523 |
+
border-radius: 50%;
|
524 |
+
z-index: 1;
|
525 |
+
}
|
526 |
+
|
527 |
+
.timeline-content {
|
528 |
+
background: rgba(255, 255, 255, 0.1);
|
529 |
+
padding: 15px;
|
530 |
+
border-radius: 8px;
|
531 |
+
margin-bottom: 5px;
|
532 |
+
transition: all var(--transition-speed) ease;
|
533 |
+
}
|
534 |
+
|
535 |
+
.timeline-content:hover {
|
536 |
+
background: rgba(255, 255, 255, 0.15);
|
537 |
+
transform: translateX(5px);
|
538 |
+
}
|
539 |
+
|
540 |
+
.timeline-content h4 {
|
541 |
+
margin: 0 0 5px;
|
542 |
+
font-size: 16px;
|
543 |
+
}
|
544 |
+
|
545 |
+
.timeline-content p {
|
546 |
+
margin: 0 0 10px;
|
547 |
+
color: var(--light-gray);
|
548 |
+
font-size: 14px;
|
549 |
+
}
|
550 |
+
|
551 |
+
.timeline-date {
|
552 |
+
font-size: 12px;
|
553 |
+
color: var(--gray);
|
554 |
+
}
|
555 |
+
|
556 |
+
.timeline-date i {
|
557 |
+
margin-right: 5px;
|
558 |
+
}
|
559 |
+
|
560 |
+
/* Action Buttons */
|
561 |
+
.action-buttons {
|
562 |
+
padding: 20px;
|
563 |
+
display: flex;
|
564 |
+
gap: 10px;
|
565 |
+
}
|
566 |
+
|
567 |
+
.save-btn, .logout-btn {
|
568 |
+
flex: 1;
|
569 |
+
padding: 14px;
|
570 |
+
border: none;
|
571 |
+
border-radius: 8px;
|
572 |
+
font-size: 15px;
|
573 |
+
font-weight: 600;
|
574 |
+
cursor: pointer;
|
575 |
+
transition: all var(--transition-speed) ease;
|
576 |
+
display: flex;
|
577 |
+
align-items: center;
|
578 |
+
justify-content: center;
|
579 |
+
}
|
580 |
+
|
581 |
+
.save-btn i, .logout-btn i {
|
582 |
+
margin-right: 8px;
|
583 |
+
font-size: 16px;
|
584 |
+
}
|
585 |
+
|
586 |
+
.save-btn {
|
587 |
+
background: var(--accent-color);
|
588 |
+
color: var(--white);
|
589 |
+
}
|
590 |
+
|
591 |
+
.save-btn:hover {
|
592 |
+
background: var(--accent-hover);
|
593 |
+
transform: translateY(-2px);
|
594 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
595 |
+
}
|
596 |
+
|
597 |
+
.logout-btn {
|
598 |
+
background: rgba(255, 255, 255, 0.1);
|
599 |
+
color: var(--danger-color);
|
600 |
+
}
|
601 |
+
|
602 |
+
.logout-btn:hover {
|
603 |
+
background: var(--danger-color);
|
604 |
+
color: var(--white);
|
605 |
+
transform: translateY(-2px);
|
606 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
607 |
+
}
|
608 |
+
|
609 |
+
/* Custom Scrollbar */
|
610 |
+
.profile-dropdown::-webkit-scrollbar {
|
611 |
+
width: 6px;
|
612 |
+
}
|
613 |
+
|
614 |
+
.profile-dropdown::-webkit-scrollbar-track {
|
615 |
+
background: rgba(0, 0, 0, 0.1);
|
616 |
+
}
|
617 |
+
|
618 |
+
.profile-dropdown::-webkit-scrollbar-thumb {
|
619 |
+
background: rgba(52, 152, 219, 0.5);
|
620 |
+
border-radius: 10px;
|
621 |
+
}
|
622 |
+
|
623 |
+
.profile-dropdown::-webkit-scrollbar-thumb:hover {
|
624 |
+
background: rgba(52, 152, 219, 0.8);
|
625 |
+
}
|
626 |
+
|
627 |
+
/* Responsive Adjustments */
|
628 |
+
@media (max-width: 600px) {
|
629 |
+
.profile-dropdown {
|
630 |
+
width: 100%;
|
631 |
+
right: -100%;
|
632 |
+
}
|
633 |
+
|
634 |
+
.activity-summary {
|
635 |
+
flex-direction: column;
|
636 |
+
gap: 10px;
|
637 |
+
}
|
638 |
+
|
639 |
+
.activity-stat {
|
640 |
+
margin: 0;
|
641 |
+
}
|
642 |
+
}
|
643 |
+
|
644 |
+
/* Animation for the dot pulse effect */
|
645 |
+
@keyframes pulse {
|
646 |
+
0% { transform: scale(1); opacity: 1; }
|
647 |
+
50% { transform: scale(1.5); opacity: 0.5; }
|
648 |
+
100% { transform: scale(1); opacity: 1; }
|
649 |
+
}
|
650 |
+
|
651 |
+
.timeline-dot.pulse {
|
652 |
+
animation: pulse 2s infinite;
|
653 |
+
}
|
654 |
+
|
static/js/.env
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
MONGO_URL= "mongodb+srv://darahasviit:<IrYaA4lePAABb0e7>@paperlens.q42bk.mongodb.net/"
|
static/js/gra.js
ADDED
@@ -0,0 +1,1080 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Initialization
|
2 |
+
let svg = d3.select("#container").append("svg")
|
3 |
+
.attr("width", "100%")
|
4 |
+
.attr("height", "100%"),
|
5 |
+
width = window.innerWidth,
|
6 |
+
height = window.innerHeight,
|
7 |
+
mainGroup = svg.append("g").attr("class", "main-container"),
|
8 |
+
simulation = null,
|
9 |
+
linkDistance = 80,
|
10 |
+
chargeStrength = -120,
|
11 |
+
gravityStrength = 0.1;
|
12 |
+
|
13 |
+
// Size scaling controls
|
14 |
+
let mainNodeScale = 1.0; // Scale factor for main paper nodes
|
15 |
+
let bridgeNodeScale = 1.0; // Scale factor for bridge paper nodes
|
16 |
+
let normalNodeScale = 1.0; // Scale factor for normal nodes (cited/referenced)
|
17 |
+
|
18 |
+
// Keep track of the original data for reference
|
19 |
+
let originalData = null;
|
20 |
+
let selectedNode = null;
|
21 |
+
let nodes = [], links = [];
|
22 |
+
|
23 |
+
// Define arrow markers for directed links
|
24 |
+
svg.append("defs").append("marker")
|
25 |
+
.attr("id", "arrow")
|
26 |
+
.attr("viewBox", "0 -5 10 10")
|
27 |
+
.attr("refX", 20)
|
28 |
+
.attr("refY", 0)
|
29 |
+
.attr("markerWidth", 6)
|
30 |
+
.attr("markerHeight", 6)
|
31 |
+
.attr("orient", "auto")
|
32 |
+
.append("path")
|
33 |
+
.attr("d", "M0,-5L10,0L0,5")
|
34 |
+
.attr("fill", "#999");
|
35 |
+
|
36 |
+
// Define star shape for main papers that are bridges
|
37 |
+
svg.append("defs")
|
38 |
+
.append("path")
|
39 |
+
.attr("id", "star")
|
40 |
+
.attr("d", d3.symbol().type(d3.symbolStar).size(300)());
|
41 |
+
|
42 |
+
// Performance optimization: Set up requestAnimationFrame for simulation updates
|
43 |
+
let rafId = null;
|
44 |
+
let isSimulationRunning = false;
|
45 |
+
|
46 |
+
function startSimulationLoop() {
|
47 |
+
if (!isSimulationRunning && simulation) {
|
48 |
+
isSimulationRunning = true;
|
49 |
+
rafId = requestAnimationFrame(simulationTick);
|
50 |
+
}
|
51 |
+
}
|
52 |
+
|
53 |
+
function stopSimulationLoop() {
|
54 |
+
isSimulationRunning = false;
|
55 |
+
if (rafId !== null) {
|
56 |
+
cancelAnimationFrame(rafId);
|
57 |
+
rafId = null;
|
58 |
+
}
|
59 |
+
}
|
60 |
+
|
61 |
+
// Optimized tick function using requestAnimationFrame
|
62 |
+
function simulationTick() {
|
63 |
+
if (isSimulationRunning && simulation && simulation.alpha() > 0.001) {
|
64 |
+
simulation.tick();
|
65 |
+
updateVisualElements();
|
66 |
+
rafId = requestAnimationFrame(simulationTick);
|
67 |
+
} else {
|
68 |
+
stopSimulationLoop();
|
69 |
+
}
|
70 |
+
}
|
71 |
+
|
72 |
+
// Update visual elements without recalculating forces
|
73 |
+
function updateVisualElements() {
|
74 |
+
// Performance optimization: Use d3.select once and then update directly
|
75 |
+
const linkElements = mainGroup.selectAll(".link");
|
76 |
+
const nodeElements = mainGroup.selectAll(".node");
|
77 |
+
|
78 |
+
// Update links - faster than using attr for each property separately
|
79 |
+
linkElements.each(function(d) {
|
80 |
+
d3.select(this)
|
81 |
+
.attr("x1", d.source.x)
|
82 |
+
.attr("y1", d.source.y)
|
83 |
+
.attr("x2", d.target.x)
|
84 |
+
.attr("y2", d.target.y);
|
85 |
+
});
|
86 |
+
|
87 |
+
// Update nodes - faster transform operation
|
88 |
+
nodeElements.attr("transform", d => `translate(${d.x}, ${d.y})`);
|
89 |
+
}
|
90 |
+
|
91 |
+
// Debounce function to improve performance
|
92 |
+
function debounce(func, wait) {
|
93 |
+
let timeout;
|
94 |
+
return function(...args) {
|
95 |
+
const context = this;
|
96 |
+
clearTimeout(timeout);
|
97 |
+
timeout = setTimeout(() => func.apply(context, args), wait);
|
98 |
+
};
|
99 |
+
}
|
100 |
+
|
101 |
+
// Cache for node radius calculations
|
102 |
+
const nodeRadiusCache = new Map();
|
103 |
+
|
104 |
+
// Event listeners for controls - optimized with debouncing
|
105 |
+
d3.select("#linkDistanceSlider").on("input", debounce(function() {
|
106 |
+
linkDistance = +this.value;
|
107 |
+
d3.select("#linkDistanceValue").text(linkDistance);
|
108 |
+
if (simulation) {
|
109 |
+
simulation.force("link").distance(linkDistance);
|
110 |
+
simulation.alpha(0.3).restart();
|
111 |
+
startSimulationLoop();
|
112 |
+
}
|
113 |
+
}, 50));
|
114 |
+
|
115 |
+
d3.select("#chargeSlider").on("input", debounce(function() {
|
116 |
+
chargeStrength = +this.value;
|
117 |
+
d3.select("#chargeValue").text(chargeStrength);
|
118 |
+
if (simulation) {
|
119 |
+
simulation.force("charge").strength(chargeStrength);
|
120 |
+
simulation.alpha(0.3).restart();
|
121 |
+
startSimulationLoop();
|
122 |
+
}
|
123 |
+
}, 50));
|
124 |
+
|
125 |
+
d3.select("#gravitySlider").on("input", debounce(function() {
|
126 |
+
gravityStrength = +this.value;
|
127 |
+
d3.select("#gravityValue").text(gravityStrength);
|
128 |
+
if (simulation) {
|
129 |
+
simulation.force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength));
|
130 |
+
simulation.alpha(0.3).restart();
|
131 |
+
startSimulationLoop();
|
132 |
+
}
|
133 |
+
}, 50));
|
134 |
+
|
135 |
+
// Paper details panel controls
|
136 |
+
d3.select("#closePanelButton").on("click", function() {
|
137 |
+
closeDetailsPanel();
|
138 |
+
});
|
139 |
+
|
140 |
+
|
141 |
+
|
142 |
+
|
143 |
+
|
144 |
+
|
145 |
+
|
146 |
+
|
147 |
+
|
148 |
+
|
149 |
+
|
150 |
+
|
151 |
+
|
152 |
+
// Time period tracking
|
153 |
+
let allPapers = []; // Store all papers from API
|
154 |
+
let currentTimeIndex = 0;
|
155 |
+
let timeChunks = []; // Will store our time chunks once we process the data
|
156 |
+
let isLoadingTimeChunk = false;
|
157 |
+
|
158 |
+
// New time navigation event listeners
|
159 |
+
d3.select("#prevTimeButton").on("click", function() {
|
160 |
+
if (currentTimeIndex > 0 && !isLoadingTimeChunk) {
|
161 |
+
currentTimeIndex--;
|
162 |
+
loadTimeChunk(currentTimeIndex);
|
163 |
+
updateTimeControls();
|
164 |
+
}
|
165 |
+
});
|
166 |
+
|
167 |
+
d3.select("#nextTimeButton").on("click", function() {
|
168 |
+
if (currentTimeIndex < timeChunks.length - 1 && !isLoadingTimeChunk) {
|
169 |
+
currentTimeIndex++;
|
170 |
+
loadTimeChunk(currentTimeIndex);
|
171 |
+
updateTimeControls();
|
172 |
+
}
|
173 |
+
});
|
174 |
+
|
175 |
+
d3.select("#timeSlider").on("input", function() {
|
176 |
+
if (!isLoadingTimeChunk) {
|
177 |
+
currentTimeIndex = +this.value;
|
178 |
+
loadTimeChunk(currentTimeIndex);
|
179 |
+
updateTimeControls();
|
180 |
+
}
|
181 |
+
});
|
182 |
+
|
183 |
+
// Function to update time navigation controls
|
184 |
+
function updateTimeControls() {
|
185 |
+
// Update button states
|
186 |
+
d3.select("#prevTimeButton").property("disabled", currentTimeIndex === 0);
|
187 |
+
d3.select("#nextTimeButton").property("disabled", currentTimeIndex === timeChunks.length - 1);
|
188 |
+
|
189 |
+
// Update time slider
|
190 |
+
d3.select("#timeSlider")
|
191 |
+
.property("value", currentTimeIndex)
|
192 |
+
.property("max", timeChunks.length - 1);
|
193 |
+
|
194 |
+
// Update time period indicator
|
195 |
+
if (timeChunks.length > 0) {
|
196 |
+
const currentChunk = timeChunks[currentTimeIndex];
|
197 |
+
d3.select("#currentTimePeriod")
|
198 |
+
.text(`${formatDate(currentChunk.startDate)} - ${formatDate(currentChunk.endDate)}`);
|
199 |
+
|
200 |
+
// Update start and end labels for the entire range
|
201 |
+
if (timeChunks.length > 0) {
|
202 |
+
d3.select("#startTimeLabel").text(formatDate(timeChunks[0].startDate));
|
203 |
+
d3.select("#endTimeLabel").text(formatDate(timeChunks[timeChunks.length - 1].endDate));
|
204 |
+
}
|
205 |
+
}
|
206 |
+
}
|
207 |
+
|
208 |
+
|
209 |
+
|
210 |
+
// Function to close the details panel
|
211 |
+
function closeDetailsPanel() {
|
212 |
+
d3.select("#paperDetailsPanel").style("display", "none");
|
213 |
+
|
214 |
+
// Deselect any selected node
|
215 |
+
if (selectedNode) {
|
216 |
+
d3.selectAll(".node").classed("selected", false);
|
217 |
+
selectedNode = null;
|
218 |
+
}
|
219 |
+
}
|
220 |
+
|
221 |
+
// Function to update node sizes when scale sliders change - optimized
|
222 |
+
function updateNodeSizes() {
|
223 |
+
if (!simulation) return;
|
224 |
+
|
225 |
+
// Performance optimization: Batch DOM updates
|
226 |
+
// Create a temporary object to store updates
|
227 |
+
const updates = [];
|
228 |
+
|
229 |
+
// Prepare all updates without touching the DOM
|
230 |
+
mainGroup.selectAll(".node").each(function(d) {
|
231 |
+
const element = d3.select(this);
|
232 |
+
const radius = getNodeRadius(d);
|
233 |
+
|
234 |
+
updates.push({
|
235 |
+
element,
|
236 |
+
d,
|
237 |
+
radius
|
238 |
+
});
|
239 |
+
});
|
240 |
+
|
241 |
+
// Now apply all updates in a batch
|
242 |
+
updates.forEach(update => {
|
243 |
+
const { element, d, radius } = update;
|
244 |
+
|
245 |
+
if (d.type === 'main' && d.isBridge) {
|
246 |
+
// Main bridge papers are stars - scale the star symbol
|
247 |
+
const starSize = radius * radius * 3;
|
248 |
+
element.select("path")
|
249 |
+
.attr("d", d3.symbol().type(d3.symbolStar).size(starSize)());
|
250 |
+
} else if (d.isBridge) {
|
251 |
+
// Other bridge papers remain diamonds
|
252 |
+
element.select("rect")
|
253 |
+
.attr("width", radius * 2)
|
254 |
+
.attr("height", radius * 2)
|
255 |
+
.attr("transform", `rotate(45) translate(${-radius}, ${-radius})`);
|
256 |
+
} else {
|
257 |
+
// Regular papers stay as circles
|
258 |
+
element.select("circle")
|
259 |
+
.attr("r", radius);
|
260 |
+
}
|
261 |
+
});
|
262 |
+
|
263 |
+
// Update the collision detection radius
|
264 |
+
simulation.force("collide").radius(d => getNodeRadius(d) * 1.5);
|
265 |
+
|
266 |
+
// Use a lower alpha value to make the transition smoother
|
267 |
+
simulation.alpha(0.1).restart();
|
268 |
+
startSimulationLoop();
|
269 |
+
}
|
270 |
+
|
271 |
+
// Debounced version of updateNodeSizes for better performance
|
272 |
+
const debouncedUpdateNodeSizes = debounce(updateNodeSizes, 200);
|
273 |
+
|
274 |
+
// Add slider event listeners for node scaling with debouncing
|
275 |
+
d3.select("#mainNodeScaleSlider").on("input", debounce(function() {
|
276 |
+
mainNodeScale = +this.value;
|
277 |
+
d3.select("#mainNodeScaleValue").text(mainNodeScale.toFixed(1));
|
278 |
+
nodeRadiusCache.clear(); // Clear cache when scale changes
|
279 |
+
debouncedUpdateNodeSizes();
|
280 |
+
}, 100));
|
281 |
+
|
282 |
+
d3.select("#bridgeNodeScaleSlider").on("input", debounce(function() {
|
283 |
+
bridgeNodeScale = +this.value;
|
284 |
+
d3.select("#bridgeNodeScaleValue").text(bridgeNodeScale.toFixed(1));
|
285 |
+
nodeRadiusCache.clear(); // Clear cache when scale changes
|
286 |
+
debouncedUpdateNodeSizes();
|
287 |
+
}, 100));
|
288 |
+
|
289 |
+
d3.select("#normalNodeScaleSlider").on("input", debounce(function() {
|
290 |
+
normalNodeScale = +this.value;
|
291 |
+
d3.select("#normalNodeScaleValue").text(normalNodeScale.toFixed(1));
|
292 |
+
nodeRadiusCache.clear(); // Clear cache when scale changes
|
293 |
+
debouncedUpdateNodeSizes();
|
294 |
+
}, 100));
|
295 |
+
|
296 |
+
d3.select("#resetButton").on("click", function() {
|
297 |
+
if (simulation) {
|
298 |
+
// Reset node positions to random positions near the center
|
299 |
+
simulation.nodes().forEach(node => {
|
300 |
+
node.x = width / 2 + (Math.random() - 0.5) * 100;
|
301 |
+
node.y = height / 2 + (Math.random() - 0.5) * 100;
|
302 |
+
node.vx = 0;
|
303 |
+
node.vy = 0;
|
304 |
+
});
|
305 |
+
simulation.alpha(1).restart();
|
306 |
+
startSimulationLoop();
|
307 |
+
}
|
308 |
+
});
|
309 |
+
|
310 |
+
// Create zoom behavior early to avoid recreating it
|
311 |
+
const zoom = d3.zoom()
|
312 |
+
.scaleExtent([0.1, 8])
|
313 |
+
.on("zoom", (event) => {
|
314 |
+
// Performance optimization: Use transform attribute instead of attr
|
315 |
+
mainGroup.attr("transform", event.transform);
|
316 |
+
});
|
317 |
+
|
318 |
+
svg.call(zoom);
|
319 |
+
|
320 |
+
d3.select("#centerViewButton").on("click", function() {
|
321 |
+
// Reset zoom and center the view
|
322 |
+
svg.transition()
|
323 |
+
.duration(750)
|
324 |
+
.call(zoom.transform, d3.zoomIdentity.translate(width/2, height/2).scale(1));
|
325 |
+
});
|
326 |
+
|
327 |
+
// Optimize resize handler with debounce
|
328 |
+
const handleResize = debounce(function() {
|
329 |
+
width = window.innerWidth;
|
330 |
+
height = window.innerHeight;
|
331 |
+
svg.attr("width", width).attr("height", height);
|
332 |
+
if (simulation) {
|
333 |
+
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
334 |
+
simulation.alpha(0.3).restart();
|
335 |
+
startSimulationLoop();
|
336 |
+
}
|
337 |
+
}, 100);
|
338 |
+
|
339 |
+
// Handle window resize
|
340 |
+
window.addEventListener('resize', handleResize);
|
341 |
+
|
342 |
+
// File input handling
|
343 |
+
async function loadCitationData() {
|
344 |
+
const progressBar = d3.select("#loadingProgress");
|
345 |
+
const statusMessage = d3.select("#statusMessage");
|
346 |
+
|
347 |
+
try {
|
348 |
+
statusMessage.text("Fetching citation data...");
|
349 |
+
progressBar.style("width", "0%");
|
350 |
+
|
351 |
+
// Simulate progress updates
|
352 |
+
const interval = setInterval(() => {
|
353 |
+
const currentWidth = parseFloat(progressBar.style("width")) || 0;
|
354 |
+
if (currentWidth < 90) {
|
355 |
+
progressBar.style("width", `${currentWidth + 10}%`);
|
356 |
+
}
|
357 |
+
}, 200);
|
358 |
+
|
359 |
+
// // Fetch all data from the backend
|
360 |
+
// const response = await fetch('http://localhost:8000/citation-data?userId=abc123&topic=ai in phishing&year=2023');
|
361 |
+
// if (!response.ok) {
|
362 |
+
// throw new Error(`Failed to load data: ${response.status}`);
|
363 |
+
// }
|
364 |
+
|
365 |
+
// const data = await response.json();
|
366 |
+
clearInterval(interval);
|
367 |
+
progressBar.style("width", "100%");
|
368 |
+
|
369 |
+
statusMessage.text("Processing data...");
|
370 |
+
|
371 |
+
const dataFromBackend = window.__INITIAL_DATA__;
|
372 |
+
|
373 |
+
|
374 |
+
console.log(dataFromBackend , " here data")
|
375 |
+
allPapers=dataFromBackend
|
376 |
+
|
377 |
+
// Process papers into time chunks (3-month periods)
|
378 |
+
processTimeChunks(allPapers);
|
379 |
+
|
380 |
+
// Load the first time chunk
|
381 |
+
if (timeChunks.length > 0) {
|
382 |
+
loadTimeChunk(0);
|
383 |
+
updateTimeControls();
|
384 |
+
}
|
385 |
+
|
386 |
+
statusMessage.text("Citation network loaded successfully!");
|
387 |
+
setTimeout(() => {
|
388 |
+
progressBar.style("display", "none");
|
389 |
+
statusMessage.style("display", "none");
|
390 |
+
d3.select(".loading-section").style("display", "none");
|
391 |
+
}, 1000);
|
392 |
+
|
393 |
+
} catch (error) {
|
394 |
+
console.error("Error loading citation data:", error);
|
395 |
+
statusMessage.text("Failed to load citation network.");
|
396 |
+
progressBar.style("width", "0%");
|
397 |
+
}
|
398 |
+
}
|
399 |
+
|
400 |
+
// Process papers into 3-month time chunks
|
401 |
+
function processTimeChunks(papers) {
|
402 |
+
// Sort papers by publication date
|
403 |
+
papers.sort((a, b) => {
|
404 |
+
const dateA = a.publication_date ? new Date(a.publication_date) : new Date(0);
|
405 |
+
const dateB = b.publication_date ? new Date(b.publication_date) : new Date(0);
|
406 |
+
return dateA - dateB;
|
407 |
+
});
|
408 |
+
|
409 |
+
// Find min and max dates
|
410 |
+
let minDate = new Date();
|
411 |
+
let maxDate = new Date(0);
|
412 |
+
|
413 |
+
papers.forEach(paper => {
|
414 |
+
if (paper.publication_date) {
|
415 |
+
const date = new Date(paper.publication_date);
|
416 |
+
if (date < minDate) minDate = new Date(date);
|
417 |
+
if (date > maxDate) maxDate = new Date(date);
|
418 |
+
}
|
419 |
+
});
|
420 |
+
|
421 |
+
// Create 3-month chunks
|
422 |
+
const chunks = [];
|
423 |
+
let currentStart = new Date(minDate);
|
424 |
+
|
425 |
+
while (currentStart < maxDate) {
|
426 |
+
// Calculate end date (3 months later)
|
427 |
+
const endDate = new Date(currentStart);
|
428 |
+
endDate.setMonth(endDate.getMonth() + 3);
|
429 |
+
|
430 |
+
// Create the chunk
|
431 |
+
chunks.push({
|
432 |
+
startDate: new Date(currentStart),
|
433 |
+
endDate: new Date(endDate),
|
434 |
+
papers: []
|
435 |
+
});
|
436 |
+
|
437 |
+
// Move to next period
|
438 |
+
currentStart = new Date(endDate);
|
439 |
+
}
|
440 |
+
|
441 |
+
// Assign papers to chunks
|
442 |
+
papers.forEach(paper => {
|
443 |
+
if (paper.publication_date) {
|
444 |
+
const pubDate = new Date(paper.publication_date);
|
445 |
+
|
446 |
+
for (let i = 0; i < chunks.length; i++) {
|
447 |
+
const chunk = chunks[i];
|
448 |
+
if (pubDate >= chunk.startDate && pubDate < chunk.endDate) {
|
449 |
+
chunk.papers.push(paper);
|
450 |
+
break;
|
451 |
+
}
|
452 |
+
}
|
453 |
+
}
|
454 |
+
});
|
455 |
+
|
456 |
+
// Remove empty chunks
|
457 |
+
timeChunks = chunks.filter(chunk => chunk.papers.length > 0);
|
458 |
+
|
459 |
+
// Update the slider max value
|
460 |
+
d3.select("#timeSlider").property("max", timeChunks.length - 1);
|
461 |
+
}
|
462 |
+
|
463 |
+
// Load a specific time chunk
|
464 |
+
function loadTimeChunk(index) {
|
465 |
+
if (isLoadingTimeChunk || index < 0 || index >= timeChunks.length) return;
|
466 |
+
|
467 |
+
isLoadingTimeChunk = true;
|
468 |
+
|
469 |
+
// Show loading indicator
|
470 |
+
d3.select("#timeLoader").style("width", "0%");
|
471 |
+
|
472 |
+
// Animate loading
|
473 |
+
const loadingInterval = setInterval(() => {
|
474 |
+
const currentWidth = parseFloat(d3.select("#timeLoader").style("width")) || 0;
|
475 |
+
if (currentWidth < 90) {
|
476 |
+
d3.select("#timeLoader").style("width", `${currentWidth + 10}%`);
|
477 |
+
}
|
478 |
+
}, 50);
|
479 |
+
|
480 |
+
// Use setTimeout to allow the UI to update before processing
|
481 |
+
setTimeout(() => {
|
482 |
+
// Process the current chunk
|
483 |
+
const chunkData = timeChunks[index].papers;
|
484 |
+
processData(chunkData);
|
485 |
+
|
486 |
+
// Complete loading
|
487 |
+
clearInterval(loadingInterval);
|
488 |
+
d3.select("#timeLoader").style("width", "100%");
|
489 |
+
|
490 |
+
// Hide loader after transition
|
491 |
+
setTimeout(() => {
|
492 |
+
d3.select("#timeLoader").style("width", "0%");
|
493 |
+
isLoadingTimeChunk = false;
|
494 |
+
}, 500);
|
495 |
+
|
496 |
+
}, 100);
|
497 |
+
}
|
498 |
+
|
499 |
+
|
500 |
+
|
501 |
+
document.addEventListener("DOMContentLoaded", function() {
|
502 |
+
loadCitationData();
|
503 |
+
});
|
504 |
+
|
505 |
+
function processData(data) {
|
506 |
+
// Stop any running simulation
|
507 |
+
if (simulation) {
|
508 |
+
simulation.stop();
|
509 |
+
stopSimulationLoop();
|
510 |
+
}
|
511 |
+
|
512 |
+
// Reset the SVG content but keep the main structure
|
513 |
+
mainGroup.selectAll("*").remove();
|
514 |
+
svg.select("defs").remove();
|
515 |
+
|
516 |
+
// Close any open panels
|
517 |
+
closeDetailsPanel();
|
518 |
+
|
519 |
+
// Redefine markers and star shape
|
520 |
+
svg.append("defs").append("marker")
|
521 |
+
.attr("id", "arrow")
|
522 |
+
.attr("viewBox", "0 -5 10 10")
|
523 |
+
.attr("refX", 20)
|
524 |
+
.attr("refY", 0)
|
525 |
+
.attr("markerWidth", 6)
|
526 |
+
.attr("markerHeight", 6)
|
527 |
+
.attr("orient", "auto")
|
528 |
+
.append("path")
|
529 |
+
.attr("d", "M0,-5L10,0L0,5")
|
530 |
+
.attr("fill", "#999");
|
531 |
+
|
532 |
+
svg.append("defs")
|
533 |
+
.append("path")
|
534 |
+
.attr("id", "star")
|
535 |
+
.attr("d", d3.symbol().type(d3.symbolStar).size(300)());
|
536 |
+
|
537 |
+
// Clear cache when loading new data
|
538 |
+
nodeRadiusCache.clear();
|
539 |
+
|
540 |
+
// Process the data to create nodes and links
|
541 |
+
nodes = [];
|
542 |
+
links = [];
|
543 |
+
const nodeMap = new Map();
|
544 |
+
|
545 |
+
// Count occurrences of each paper ID to identify bridge papers
|
546 |
+
const paperOccurrences = new Map();
|
547 |
+
|
548 |
+
// Performance optimization: Pre-allocate array size when possible
|
549 |
+
// First pass: Count occurrences of papers in references and citations
|
550 |
+
data.forEach(paper => {
|
551 |
+
// Count the main paper
|
552 |
+
incrementPaperCount(paperOccurrences, paper.id);
|
553 |
+
|
554 |
+
// Count referenced papers
|
555 |
+
if (paper.referenced_works) {
|
556 |
+
paper.referenced_works.forEach(ref => {
|
557 |
+
incrementPaperCount(paperOccurrences, ref);
|
558 |
+
});
|
559 |
+
}
|
560 |
+
|
561 |
+
// Count citing papers
|
562 |
+
if (paper.cited_by_ids) {
|
563 |
+
paper.cited_by_ids.forEach(cite => {
|
564 |
+
incrementPaperCount(paperOccurrences, cite);
|
565 |
+
});
|
566 |
+
}
|
567 |
+
});
|
568 |
+
|
569 |
+
// Performance optimization: Estimate total node and link count
|
570 |
+
let estimatedNodeCount = data.length;
|
571 |
+
let estimatedLinkCount = 0;
|
572 |
+
|
573 |
+
data.forEach(paper => {
|
574 |
+
if (paper.referenced_works) estimatedNodeCount += paper.referenced_works.length;
|
575 |
+
if (paper.cited_by_ids) estimatedNodeCount += paper.cited_by_ids.length;
|
576 |
+
|
577 |
+
if (paper.referenced_works) estimatedLinkCount += paper.referenced_works.length;
|
578 |
+
if (paper.cited_by_ids) estimatedLinkCount += paper.cited_by_ids.length;
|
579 |
+
});
|
580 |
+
|
581 |
+
// Pre-allocate arrays
|
582 |
+
nodes = new Array(estimatedNodeCount);
|
583 |
+
links = new Array(estimatedLinkCount);
|
584 |
+
let nodeIndex = 0;
|
585 |
+
let linkIndex = 0;
|
586 |
+
|
587 |
+
// Second pass: Create nodes and links
|
588 |
+
data.forEach(paper => {
|
589 |
+
// Add main paper node if not already in the map
|
590 |
+
if (!nodeMap.has(paper.id)) {
|
591 |
+
const node = {
|
592 |
+
id: paper.id,
|
593 |
+
title: paper.title || "Unknown Title",
|
594 |
+
cited_by_count: paper.cited_by_count || 0,
|
595 |
+
concepts: paper.concepts || [],
|
596 |
+
publication_date: paper.publication_date || null, // Add publication date
|
597 |
+
referenced_works: paper.referenced_works || [],
|
598 |
+
cited_by_ids: paper.cited_by_ids || [],
|
599 |
+
type: 'main',
|
600 |
+
isBridge: paperOccurrences.get(paper.id) > 1,
|
601 |
+
// Start main papers near the center
|
602 |
+
x: width / 2 + (Math.random() - 0.5) * 100,
|
603 |
+
y: height / 2 + (Math.random() - 0.5) * 100
|
604 |
+
};
|
605 |
+
nodes[nodeIndex++] = node;
|
606 |
+
nodeMap.set(paper.id, node);
|
607 |
+
}
|
608 |
+
|
609 |
+
// Add referenced paper nodes and create links
|
610 |
+
if (paper.referenced_works) {
|
611 |
+
paper.referenced_works.forEach(ref => {
|
612 |
+
if (!nodeMap.has(ref)) {
|
613 |
+
const node = {
|
614 |
+
id: ref,
|
615 |
+
title: "Referenced Paper",
|
616 |
+
type: 'reference',
|
617 |
+
isBridge: paperOccurrences.get(ref) > 1,
|
618 |
+
// Start near their respective main paper
|
619 |
+
x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
|
620 |
+
y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
|
621 |
+
};
|
622 |
+
nodes[nodeIndex++] = node;
|
623 |
+
nodeMap.set(ref, node);
|
624 |
+
}
|
625 |
+
|
626 |
+
links[linkIndex++] = {
|
627 |
+
source: paper.id,
|
628 |
+
target: ref,
|
629 |
+
direction: 'outgoing'
|
630 |
+
};
|
631 |
+
});
|
632 |
+
}
|
633 |
+
|
634 |
+
// Add citing paper nodes and create links
|
635 |
+
if (paper.cited_by_ids) {
|
636 |
+
paper.cited_by_ids.forEach(cite => {
|
637 |
+
if (!nodeMap.has(cite)) {
|
638 |
+
const node = {
|
639 |
+
id: cite,
|
640 |
+
title: "Citing Paper",
|
641 |
+
type: 'citation',
|
642 |
+
isBridge: paperOccurrences.get(cite) > 1,
|
643 |
+
// Start near their respective main paper
|
644 |
+
x: nodeMap.get(paper.id).x + (Math.random() - 0.5) * 50,
|
645 |
+
y: nodeMap.get(paper.id).y + (Math.random() - 0.5) * 50
|
646 |
+
};
|
647 |
+
nodes[nodeIndex++] = node;
|
648 |
+
nodeMap.set(cite, node);
|
649 |
+
}
|
650 |
+
|
651 |
+
links[linkIndex++] = {
|
652 |
+
source: cite,
|
653 |
+
target: paper.id,
|
654 |
+
direction: 'incoming'
|
655 |
+
};
|
656 |
+
});
|
657 |
+
}
|
658 |
+
});
|
659 |
+
|
660 |
+
// Trim arrays to actual size
|
661 |
+
nodes = nodes.slice(0, nodeIndex);
|
662 |
+
links = links.slice(0, linkIndex);
|
663 |
+
|
664 |
+
createVisualization(nodes, links);
|
665 |
+
}
|
666 |
+
|
667 |
+
function incrementPaperCount(map, id) {
|
668 |
+
if (!map.has(id)) {
|
669 |
+
map.set(id, 1);
|
670 |
+
} else {
|
671 |
+
map.set(id, map.get(id) + 1);
|
672 |
+
}
|
673 |
+
}
|
674 |
+
|
675 |
+
function createVisualization(nodes, links) {
|
676 |
+
// Performance optimization: Use quadtree for faster force calculation
|
677 |
+
simulation = d3.forceSimulation(nodes)
|
678 |
+
.force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
|
679 |
+
.force("charge", d3.forceManyBody().strength(chargeStrength).theta(0.8)) // Higher theta = better performance
|
680 |
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
681 |
+
.force("gravity", d3.forceRadial(0, width / 2, height / 2).strength(gravityStrength))
|
682 |
+
// Add collision detection to prevent node overlap
|
683 |
+
.force("collide", d3.forceCollide().radius(d => getNodeRadius(d) * 1.5).strength(0.7)); // Reduced strength for better performance
|
684 |
+
|
685 |
+
// Performance: Lower alpha decay for smoother but faster convergence
|
686 |
+
simulation.alphaDecay(0.02);
|
687 |
+
|
688 |
+
// Performance: Remove automatic ticking and handle it via requestAnimationFrame
|
689 |
+
simulation.on("tick", null);
|
690 |
+
|
691 |
+
// Create link group first (rendered below nodes)
|
692 |
+
const linkGroup = mainGroup.append("g").attr("class", "links");
|
693 |
+
|
694 |
+
// Performance optimization: Use object pooling for links
|
695 |
+
const link = linkGroup.selectAll("line")
|
696 |
+
.data(links)
|
697 |
+
.enter().append("line")
|
698 |
+
.attr("class", "link")
|
699 |
+
.attr("stroke", "#999")
|
700 |
+
.attr("stroke-width", 1)
|
701 |
+
.attr("marker-end", "url(#arrow)");
|
702 |
+
|
703 |
+
// Create node group
|
704 |
+
const nodeGroup = mainGroup.append("g").attr("class", "nodes");
|
705 |
+
|
706 |
+
// Create a more efficient node container
|
707 |
+
const node = nodeGroup.selectAll(".node")
|
708 |
+
.data(nodes)
|
709 |
+
.enter().append("g")
|
710 |
+
.attr("class", "node")
|
711 |
+
.call(d3.drag()
|
712 |
+
.on("start", dragstarted)
|
713 |
+
.on("drag", dragged)
|
714 |
+
.on("end", dragended)
|
715 |
+
)
|
716 |
+
.on("click", nodeClicked);
|
717 |
+
|
718 |
+
// Performance optimization: Batch DOM operations
|
719 |
+
// Add different shapes based on node type
|
720 |
+
node.each(function(d) {
|
721 |
+
const element = d3.select(this);
|
722 |
+
const radius = getNodeRadius(d);
|
723 |
+
|
724 |
+
if (d.type === 'main' && d.isBridge) {
|
725 |
+
// Main bridge papers are stars
|
726 |
+
const starSize = radius * radius * 3;
|
727 |
+
element.append("path")
|
728 |
+
.attr("d", d3.symbol().type(d3.symbolStar).size(starSize)())
|
729 |
+
.attr("fill", "#4285f4");
|
730 |
+
} else if (d.isBridge) {
|
731 |
+
// Bridge papers are diamonds
|
732 |
+
element.append("rect")
|
733 |
+
.attr("width", radius * 2)
|
734 |
+
.attr("height", radius * 2)
|
735 |
+
.attr("transform", `rotate(45) translate(${-radius}, ${-radius})`)
|
736 |
+
.attr("fill", "#fbbc05");
|
737 |
+
} else if (d.type === 'main') {
|
738 |
+
// Main papers are blue circles
|
739 |
+
element.append("circle")
|
740 |
+
.attr("r", radius)
|
741 |
+
.attr("fill", "#4285f4");
|
742 |
+
} else if (d.type === 'reference') {
|
743 |
+
// Referenced papers are green circles
|
744 |
+
element.append("circle")
|
745 |
+
.attr("r", radius)
|
746 |
+
.attr("fill", "#34a853");
|
747 |
+
} else if (d.type === 'citation') {
|
748 |
+
// Citing papers are red circles
|
749 |
+
element.append("circle")
|
750 |
+
.attr("r", radius)
|
751 |
+
.attr("fill", "#ea4335");
|
752 |
+
} else {
|
753 |
+
// Default case - grey circles
|
754 |
+
element.append("circle")
|
755 |
+
.attr("r", radius)
|
756 |
+
.attr("fill", "#999");
|
757 |
+
}
|
758 |
+
});
|
759 |
+
|
760 |
+
// Optimized tooltip handling
|
761 |
+
const tooltip = d3.select(".tooltip");
|
762 |
+
|
763 |
+
// Performance optimization: Use event delegation
|
764 |
+
// Update inside the mouseoverEvent handler in the createVisualization function
|
765 |
+
nodeGroup.on("mouseover", function(event) {
|
766 |
+
const target = event.target;
|
767 |
+
if (target.closest(".node")) {
|
768 |
+
const d = d3.select(target.closest(".node")).datum();
|
769 |
+
|
770 |
+
// Prepare tooltip content
|
771 |
+
let tooltipContent = `<strong>${d.title}</strong>`;
|
772 |
+
|
773 |
+
// Add publication date if available
|
774 |
+
if (d.publication_date) {
|
775 |
+
tooltipContent += `<br>Published: ${formatDate(d.publication_date)}`;
|
776 |
+
}
|
777 |
+
|
778 |
+
// Add citation count if available
|
779 |
+
if (d.cited_by_count) {
|
780 |
+
tooltipContent += `<br>Citations: ${d.cited_by_count}`;
|
781 |
+
}
|
782 |
+
|
783 |
+
// Add concepts if available
|
784 |
+
if (d.concepts && d.concepts.length > 0) {
|
785 |
+
tooltipContent += `<br>Concepts: ${formatConcepts(d.concepts)}`;
|
786 |
+
}
|
787 |
+
|
788 |
+
// Add bridge paper info if applicable
|
789 |
+
if (d.isBridge) {
|
790 |
+
tooltipContent += "<br><em>Bridge Paper</em>";
|
791 |
+
}
|
792 |
+
|
793 |
+
// Show tooltip with enhanced paper information
|
794 |
+
tooltip.style("display", "block")
|
795 |
+
.style("left", (event.pageX + 15) + "px")
|
796 |
+
.style("top", (event.pageY - 30) + "px")
|
797 |
+
.html(tooltipContent);
|
798 |
+
}
|
799 |
+
})
|
800 |
+
.on("mouseout", function(event) {
|
801 |
+
const target = event.target;
|
802 |
+
if (target.closest(".node")) {
|
803 |
+
tooltip.style("display", "none");
|
804 |
+
}
|
805 |
+
})
|
806 |
+
.on("mousemove", function(event) {
|
807 |
+
tooltip.style("left", (event.pageX + 15) + "px")
|
808 |
+
.style("top", (event.pageY - 30) + "px");
|
809 |
+
});
|
810 |
+
|
811 |
+
// Start simulation loop using requestAnimationFrame
|
812 |
+
startSimulationLoop();
|
813 |
+
}
|
814 |
+
|
815 |
+
// Calculate node radius based on node type and importance
|
816 |
+
function getNodeRadius(d) {
|
817 |
+
// Return from cache if already calculated
|
818 |
+
if (nodeRadiusCache.has(d.id)) {
|
819 |
+
return nodeRadiusCache.get(d.id);
|
820 |
+
}
|
821 |
+
|
822 |
+
let radius;
|
823 |
+
|
824 |
+
if (d.type === 'main') {
|
825 |
+
// Main papers: size based on citation count and scaled by mainNodeScale
|
826 |
+
const baseCitationSize = Math.max(5, Math.min(15, 5 + Math.log(d.cited_by_count + 1)));
|
827 |
+
radius = baseCitationSize * mainNodeScale;
|
828 |
+
} else if (d.isBridge) {
|
829 |
+
// Bridge papers: slightly larger than normal papers and scaled by bridgeNodeScale
|
830 |
+
radius = 6 * bridgeNodeScale;
|
831 |
+
} else {
|
832 |
+
// Referenced or citing papers: normal size and scaled by normalNodeScale
|
833 |
+
radius = 4 * normalNodeScale;
|
834 |
+
}
|
835 |
+
|
836 |
+
// Cache the result
|
837 |
+
nodeRadiusCache.set(d.id, radius);
|
838 |
+
return radius;
|
839 |
+
}
|
840 |
+
|
841 |
+
// Drag functions for nodes - optimized
|
842 |
+
function dragstarted(event, d) {
|
843 |
+
stopSimulationLoop(); // Stop animation loop during drag
|
844 |
+
if (!event.active) simulation.alphaTarget(0.3);
|
845 |
+
d.fx = d.x;
|
846 |
+
d.fy = d.y;
|
847 |
+
}
|
848 |
+
|
849 |
+
function dragged(event, d) {
|
850 |
+
d.fx = event.x;
|
851 |
+
d.fy = event.y;
|
852 |
+
|
853 |
+
// Update position immediately for smoother dragging
|
854 |
+
d3.select(event.sourceEvent.target.closest(".node"))
|
855 |
+
.attr("transform", `translate(${d.fx}, ${d.fy})`);
|
856 |
+
|
857 |
+
// Update connected links for immediate feedback
|
858 |
+
mainGroup.selectAll(".link").each(function(linkData) {
|
859 |
+
if (linkData.source === d || linkData.target === d) {
|
860 |
+
d3.select(this)
|
861 |
+
.attr("x1", linkData.source.x)
|
862 |
+
.attr("y1", linkData.source.y)
|
863 |
+
.attr("x2", linkData.target.x)
|
864 |
+
.attr("y2", linkData.target.y);
|
865 |
+
}
|
866 |
+
});
|
867 |
+
}
|
868 |
+
|
869 |
+
function dragended(event, d) {
|
870 |
+
if (!event.active) simulation.alphaTarget(0);
|
871 |
+
// Comment these out to keep the node fixed at the position where it was dragged
|
872 |
+
// d.fx = null;
|
873 |
+
// d.fy = null;
|
874 |
+
|
875 |
+
// Restart simulation loop
|
876 |
+
startSimulationLoop();
|
877 |
+
}
|
878 |
+
|
879 |
+
// Helper function to format publication date
|
880 |
+
function formatDate(dateString) {
|
881 |
+
if (!dateString) return "Unknown";
|
882 |
+
|
883 |
+
try {
|
884 |
+
const date = new Date(dateString);
|
885 |
+
// Check if date is valid
|
886 |
+
if (isNaN(date.getTime())) return dateString;
|
887 |
+
|
888 |
+
return date.toLocaleDateString('en-US', {
|
889 |
+
year: 'numeric',
|
890 |
+
month: 'short',
|
891 |
+
day: 'numeric'
|
892 |
+
});
|
893 |
+
} catch (e) {
|
894 |
+
return dateString;
|
895 |
+
}
|
896 |
+
}
|
897 |
+
|
898 |
+
// Helper function to format concepts
|
899 |
+
function formatConcepts(concepts) {
|
900 |
+
if (!concepts || concepts.length === 0) return "None";
|
901 |
+
|
902 |
+
// Handle both string concepts and concept objects
|
903 |
+
const conceptNames = concepts.map(concept => {
|
904 |
+
if (typeof concept === 'string') return concept;
|
905 |
+
return concept.display_name || concept.name || "Unknown concept";
|
906 |
+
});
|
907 |
+
|
908 |
+
// Return first 3 concepts to keep tooltip compact
|
909 |
+
if (conceptNames.length > 3) {
|
910 |
+
return conceptNames.slice(0, 3).join(", ") + ` +${conceptNames.length - 3} more`;
|
911 |
+
}
|
912 |
+
|
913 |
+
return conceptNames.join(", ");
|
914 |
+
}
|
915 |
+
|
916 |
+
// Handle node click to show paper details - optimized
|
917 |
+
// Update the nodeClicked function
|
918 |
+
function nodeClicked(event, d) {
|
919 |
+
event.stopPropagation(); // Prevent bubbling to SVG
|
920 |
+
|
921 |
+
// Set as selected node
|
922 |
+
selectedNode = d;
|
923 |
+
|
924 |
+
// Update visual selection - optimized to only change classes for relevant nodes
|
925 |
+
d3.selectAll(".node.selected").classed("selected", false);
|
926 |
+
d3.select(this).classed("selected", true);
|
927 |
+
|
928 |
+
// Get paper details from the original data if it's a main paper
|
929 |
+
let paperDetails = d;
|
930 |
+
if (d.type === 'main' && originalData) {
|
931 |
+
paperDetails = originalData.find(paper => paper.id === d.id) || d;
|
932 |
+
}
|
933 |
+
|
934 |
+
// Update paper info panel - batch DOM updates
|
935 |
+
const paperInfoUpdates = {
|
936 |
+
"#paperTitle": paperDetails.title || "Unknown Title",
|
937 |
+
"#paperCitations": paperDetails.cited_by_count !== undefined ? `Citations: ${paperDetails.cited_by_count}` : ""
|
938 |
+
};
|
939 |
+
|
940 |
+
// Add publication date if available
|
941 |
+
if (paperDetails.publication_date) {
|
942 |
+
paperInfoUpdates["#paperType"] = `${d.type === 'main' ? "Main Paper" :
|
943 |
+
d.type === 'reference' ? "Referenced Paper" :
|
944 |
+
d.type === 'citation' ? "Citing Paper" : "Paper"}${d.isBridge ? " (Bridge)" : ""} · Published: ${formatDate(paperDetails.publication_date)}`;
|
945 |
+
} else {
|
946 |
+
// Set paper type without date
|
947 |
+
let typeText = "";
|
948 |
+
if (d.type === 'main') typeText = "Main Paper";
|
949 |
+
else if (d.type === 'reference') typeText = "Referenced Paper";
|
950 |
+
else if (d.type === 'citation') typeText = "Citing Paper";
|
951 |
+
|
952 |
+
if (d.isBridge) typeText += " (Bridge)";
|
953 |
+
paperInfoUpdates["#paperType"] = typeText;
|
954 |
+
}
|
955 |
+
|
956 |
+
// Apply all text updates in one batch
|
957 |
+
Object.entries(paperInfoUpdates).forEach(([selector, text]) => {
|
958 |
+
d3.select(selector).text(text);
|
959 |
+
});
|
960 |
+
|
961 |
+
// Set concepts if available - use document fragment for better performance
|
962 |
+
const conceptsDiv = d3.select("#paperConcepts");
|
963 |
+
conceptsDiv.html("");
|
964 |
+
|
965 |
+
if (paperDetails.concepts && paperDetails.concepts.length > 0) {
|
966 |
+
// Performance: Create document fragment for batch append
|
967 |
+
const fragment = document.createDocumentFragment();
|
968 |
+
|
969 |
+
// Add a small label for concepts
|
970 |
+
const conceptLabel = document.createElement("div");
|
971 |
+
conceptLabel.className = "concept-label";
|
972 |
+
conceptLabel.textContent = "Concepts:";
|
973 |
+
fragment.appendChild(conceptLabel);
|
974 |
+
|
975 |
+
paperDetails.concepts.forEach(concept => {
|
976 |
+
const span = document.createElement("span");
|
977 |
+
span.className = "concept-tag";
|
978 |
+
span.textContent = typeof concept === 'string' ? concept : (concept.display_name || concept.name || "Unknown");
|
979 |
+
fragment.appendChild(span);
|
980 |
+
});
|
981 |
+
|
982 |
+
conceptsDiv.node().appendChild(fragment);
|
983 |
+
}
|
984 |
+
|
985 |
+
// Set OpenAlex link
|
986 |
+
d3.select("#openAlexLink")
|
987 |
+
.attr("href", `${d.id}`);
|
988 |
+
|
989 |
+
// Performance optimization: Batch DOM operations for related papers lists
|
990 |
+
// Use setTimeout to defer heavy operations and keep UI responsive
|
991 |
+
setTimeout(() => {
|
992 |
+
populateRelatedPapersList(paperDetails, "citing");
|
993 |
+
populateRelatedPapersList(paperDetails, "referenced");
|
994 |
+
|
995 |
+
// Show the panel
|
996 |
+
d3.select("#paperDetailsPanel").style("display", "block");
|
997 |
+
}, 0);
|
998 |
+
}
|
999 |
+
|
1000 |
+
// Populate the lists of citing and referenced papers - optimized
|
1001 |
+
function populateRelatedPapersList(paperDetails, listType) {
|
1002 |
+
const listElement = listType === "citing" ?
|
1003 |
+
d3.select("#citingPapersList") :
|
1004 |
+
d3.select("#referencedPapersList");
|
1005 |
+
|
1006 |
+
listElement.html("");
|
1007 |
+
|
1008 |
+
const paperIds = listType === "citing" ?
|
1009 |
+
paperDetails.cited_by_ids :
|
1010 |
+
paperDetails.referenced_works;
|
1011 |
+
|
1012 |
+
if (!paperIds || paperIds.length === 0) {
|
1013 |
+
listElement.append("li")
|
1014 |
+
.text("No papers available");
|
1015 |
+
return;
|
1016 |
+
}
|
1017 |
+
|
1018 |
+
// Performance optimization: Use document fragment for batch DOM operations
|
1019 |
+
const fragment = document.createDocumentFragment();
|
1020 |
+
|
1021 |
+
// Get paper details if available in original data
|
1022 |
+
paperIds.forEach(id => {
|
1023 |
+
const li = document.createElement("li");
|
1024 |
+
|
1025 |
+
// Try to find paper details in original data
|
1026 |
+
let paperInfo = null;
|
1027 |
+
if (originalData) {
|
1028 |
+
paperInfo = originalData.find(paper => paper.id === id);
|
1029 |
+
}
|
1030 |
+
|
1031 |
+
const a = document.createElement("a");
|
1032 |
+
if (paperInfo) {
|
1033 |
+
a.href = "#";
|
1034 |
+
a.setAttribute("data-id", id);
|
1035 |
+
a.textContent = paperInfo.title || id;
|
1036 |
+
a.addEventListener("click", function(event) {
|
1037 |
+
event.preventDefault();
|
1038 |
+
const paperId = this.getAttribute("data-id");
|
1039 |
+
const nodeElement = mainGroup.selectAll(".node").filter(d => d.id === paperId);
|
1040 |
+
if (!nodeElement.empty()) {
|
1041 |
+
nodeClicked.call(nodeElement.node(), event, nodeElement.datum());
|
1042 |
+
}
|
1043 |
+
});
|
1044 |
+
} else {
|
1045 |
+
a.href = `${id}`;
|
1046 |
+
a.target = "_blank";
|
1047 |
+
a.textContent = id;
|
1048 |
+
}
|
1049 |
+
|
1050 |
+
li.appendChild(a);
|
1051 |
+
fragment.appendChild(li);
|
1052 |
+
});
|
1053 |
+
|
1054 |
+
listElement.node().appendChild(fragment);
|
1055 |
+
}
|
1056 |
+
|
1057 |
+
// Close details panel when clicking on the background
|
1058 |
+
svg.on("click", function() {
|
1059 |
+
closeDetailsPanel();
|
1060 |
+
});
|
1061 |
+
|
1062 |
+
// Clean up resources when window is closed or component is unmounted
|
1063 |
+
window.addEventListener('beforeunload', function() {
|
1064 |
+
if (simulation) {
|
1065 |
+
simulation.stop();
|
1066 |
+
}
|
1067 |
+
stopSimulationLoop();
|
1068 |
+
});
|
1069 |
+
|
1070 |
+
// Load sample data or wait for user upload
|
1071 |
+
document.addEventListener("DOMContentLoaded", function() {
|
1072 |
+
// Optional: Load sample data automatically
|
1073 |
+
// fetch('sample_data.json')
|
1074 |
+
// .then(response => response.json())
|
1075 |
+
// .then(data => {
|
1076 |
+
// originalData = data;
|
1077 |
+
// processData(data);
|
1078 |
+
// })
|
1079 |
+
// .catch(error => console.error('Error loading sample data:', error));
|
1080 |
+
});
|
static/js/home.js
ADDED
@@ -0,0 +1,1627 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const getLocalUser = localStorage.getItem("username");
|
2 |
+
if (getLocalUser) {
|
3 |
+
document.getElementById("userProfile").innerHTML = `${getLocalUser}`;
|
4 |
+
} else {
|
5 |
+
document.getElementById("userProfile").innerHTML = "Hei";
|
6 |
+
}
|
7 |
+
|
8 |
+
function populateYearDropdown(startYear, endYear) {
|
9 |
+
const dropdown = document.getElementById('year-filter');
|
10 |
+
|
11 |
+
// Clear existing options (except the first default option)
|
12 |
+
while (dropdown.options.length > 1) {
|
13 |
+
dropdown.remove(1);
|
14 |
+
}
|
15 |
+
|
16 |
+
// Add years dynamically
|
17 |
+
for (let year = endYear; year >= startYear; year--) {
|
18 |
+
const option = document.createElement('option');
|
19 |
+
option.value = year;
|
20 |
+
option.textContent = year;
|
21 |
+
dropdown.appendChild(option);
|
22 |
+
}
|
23 |
+
}
|
24 |
+
|
25 |
+
// Populate the dropdown with years from 1900 to 2025
|
26 |
+
populateYearDropdown(1900, 2025);
|
27 |
+
|
28 |
+
const profileContainer = document.getElementById("profileContainer");
|
29 |
+
const signInBtn = document.getElementById("signInBtn");
|
30 |
+
const storedUsername = localStorage.getItem("username");
|
31 |
+
const storedUserId = localStorage.getItem("userId")
|
32 |
+
|
33 |
+
if (storedUsername) {
|
34 |
+
profileContainer.style.display = "flex";
|
35 |
+
signInBtn.style.display = "none";
|
36 |
+
} else {
|
37 |
+
profileContainer.style.display = "none";
|
38 |
+
signInBtn.style.display = "block";
|
39 |
+
}
|
40 |
+
|
41 |
+
// Elements for profile dropdown and overlay
|
42 |
+
const userProfile = document.getElementById("userProfile");
|
43 |
+
const profileDropdown = document.getElementById("profileDropdown");
|
44 |
+
const profileOverlay = document.getElementById("profileOverlay");
|
45 |
+
const logoutBtn = document.getElementById("logoutBtn");
|
46 |
+
|
47 |
+
// Toggle dropdown on clicking profile trigger
|
48 |
+
userProfile.addEventListener("click", () => {
|
49 |
+
profileDropdown.classList.add("active");
|
50 |
+
profileOverlay.classList.add("active");
|
51 |
+
});
|
52 |
+
|
53 |
+
// Close dropdown when clicking outside
|
54 |
+
profileOverlay.addEventListener("click", () => {
|
55 |
+
profileDropdown.classList.remove("active");
|
56 |
+
profileOverlay.classList.remove("active");
|
57 |
+
});
|
58 |
+
|
59 |
+
async function saveSearchToFirebase(query, year, feature) {
|
60 |
+
const auth = firebase.auth();
|
61 |
+
const db = firebase.firestore();
|
62 |
+
const user = auth.currentUser;
|
63 |
+
if (!user) {
|
64 |
+
console.log("User not logged in, cannot save search history");
|
65 |
+
return;
|
66 |
+
}
|
67 |
+
|
68 |
+
try {
|
69 |
+
// Create search activity object
|
70 |
+
const searchData = {
|
71 |
+
type: "search",
|
72 |
+
query: query,
|
73 |
+
year: year || "All Years",
|
74 |
+
feature: feature,
|
75 |
+
timestamp: firebase.firestore.FieldValue.serverTimestamp()
|
76 |
+
};
|
77 |
+
|
78 |
+
// Add to Firestore
|
79 |
+
await db.collection("users")
|
80 |
+
.doc(user.uid)
|
81 |
+
.collection("activities")
|
82 |
+
.add(searchData);
|
83 |
+
|
84 |
+
console.log("Search activity saved successfully");
|
85 |
+
} catch (error) {
|
86 |
+
console.error("Error saving search activity:", error);
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
|
91 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
92 |
+
// Input fields for personal info
|
93 |
+
const profileName = document.getElementById("profileName");
|
94 |
+
const profileEmail = document.getElementById("profileEmail");
|
95 |
+
const profilePhone = document.getElementById("profilePhone");
|
96 |
+
const profileAddress = document.getElementById("profileAddress");
|
97 |
+
const profileProfession = document.getElementById("profileProfession");
|
98 |
+
const profileHeaderName = document.getElementById("profileHeaderName");
|
99 |
+
|
100 |
+
// Firebase Auth & Firestore setup
|
101 |
+
const auth = firebase.auth();
|
102 |
+
const db = firebase.firestore();
|
103 |
+
|
104 |
+
auth.onAuthStateChanged(async (user) => {
|
105 |
+
if (user) {
|
106 |
+
const userId = user.uid;
|
107 |
+
|
108 |
+
// Load saved personal info from localStorage
|
109 |
+
profileName.value = localStorage.getItem("username") || "";
|
110 |
+
profilePhone.value = localStorage.getItem("phone") || "";
|
111 |
+
profileAddress.value = localStorage.getItem("address") || "";
|
112 |
+
profileProfession.value = localStorage.getItem("profession") || "";
|
113 |
+
profileHeaderName.innerHTML = `Welcome, ${storedUsername}`;
|
114 |
+
|
115 |
+
|
116 |
+
|
117 |
+
try {
|
118 |
+
// Fetch additional user details from Firestore
|
119 |
+
const userDoc = await db.collection("users").doc(userId).get();
|
120 |
+
if (userDoc.exists) {
|
121 |
+
const userData = userDoc.data();
|
122 |
+
if (!profileName.value) profileName.value = userData.name || "";
|
123 |
+
if (!profilePhone.value) profilePhone.value = userData.phone || "";
|
124 |
+
if (!profileAddress.value) profileAddress.value = userData.address || "";
|
125 |
+
if (!profileProfession.value) profileProfession.value = userData.profession || "";
|
126 |
+
}
|
127 |
+
} catch (error) {
|
128 |
+
console.error("Error fetching user data:", error);
|
129 |
+
}
|
130 |
+
}
|
131 |
+
});
|
132 |
+
|
133 |
+
// Enable editing for profile fields
|
134 |
+
window.enableEdit = (fieldId) => {
|
135 |
+
const inputField = document.getElementById(fieldId);
|
136 |
+
inputField.removeAttribute("disabled");
|
137 |
+
inputField.focus();
|
138 |
+
inputField.addEventListener("blur", () => {
|
139 |
+
// When leaving, set back to readonly (or disabled if you prefer)
|
140 |
+
inputField.setAttribute("disabled", true);
|
141 |
+
});
|
142 |
+
};
|
143 |
+
|
144 |
+
// Tab switching for profile dropdown
|
145 |
+
const tabButtons = document.querySelectorAll(".tab-btn");
|
146 |
+
const tabContents = document.querySelectorAll(".tab-content");
|
147 |
+
|
148 |
+
tabButtons.forEach((btn) => {
|
149 |
+
btn.addEventListener("click", () => {
|
150 |
+
const tab = btn.getAttribute("data-tab");
|
151 |
+
tabButtons.forEach((b) => b.classList.remove("active"));
|
152 |
+
tabContents.forEach((c) => c.classList.remove("active"));
|
153 |
+
btn.classList.add("active");
|
154 |
+
document.getElementById(`${tab}-tab`).classList.add("active");
|
155 |
+
});
|
156 |
+
});
|
157 |
+
|
158 |
+
// Avatar upload preview
|
159 |
+
const avatarUpload = document.getElementById("avatarUpload");
|
160 |
+
const profileAvatar = document.getElementById("profileAvatar");
|
161 |
+
|
162 |
+
if (avatarUpload) {
|
163 |
+
avatarUpload.addEventListener("change", (e) => {
|
164 |
+
const file = e.target.files[0];
|
165 |
+
if (file) {
|
166 |
+
const reader = new FileReader();
|
167 |
+
reader.onload = (event) => {
|
168 |
+
profileAvatar.src = event.target.result;
|
169 |
+
// (Optional) Save the avatar image to your storage if needed
|
170 |
+
};
|
171 |
+
reader.readAsDataURL(file);
|
172 |
+
}
|
173 |
+
});
|
174 |
+
}
|
175 |
+
|
176 |
+
// Save profile details and preferences
|
177 |
+
const saveBtn = document.getElementById("saveBtn");
|
178 |
+
saveBtn.addEventListener("click", async () => {
|
179 |
+
const user = auth.currentUser;
|
180 |
+
if (!user) {
|
181 |
+
alert("No user logged in!");
|
182 |
+
return;
|
183 |
+
}
|
184 |
+
const userId = user.uid;
|
185 |
+
|
186 |
+
// Gather updated personal info
|
187 |
+
const updatedName = profileName.value.trim();
|
188 |
+
const updatedPhone = profilePhone.value.trim();
|
189 |
+
const updatedAddress = profileAddress.value.trim();
|
190 |
+
const updatedProfession = profileProfession.value.trim();
|
191 |
+
|
192 |
+
// Save personal info in localStorage
|
193 |
+
localStorage.setItem("username", updatedName);
|
194 |
+
localStorage.setItem("phone", updatedPhone);
|
195 |
+
localStorage.setItem("address", updatedAddress);
|
196 |
+
localStorage.setItem("profession", updatedProfession);
|
197 |
+
|
198 |
+
// Store personal info in Firestore
|
199 |
+
try {
|
200 |
+
await db.collection("users").doc(userId).set(
|
201 |
+
{
|
202 |
+
name: updatedName,
|
203 |
+
phone: updatedPhone,
|
204 |
+
address: updatedAddress,
|
205 |
+
profession: updatedProfession,
|
206 |
+
|
207 |
+
},
|
208 |
+
{ merge: true }
|
209 |
+
);
|
210 |
+
userProfile.textContent = updatedName; // Update navbar username
|
211 |
+
showPaperAlert(
|
212 |
+
"Successfully Updated!",
|
213 |
+
"Profile Updation Status",
|
214 |
+
"fa-exclamation-circle",
|
215 |
+
4000
|
216 |
+
);
|
217 |
+
} catch (error) {
|
218 |
+
console.error("Error updating profile:", error);
|
219 |
+
showPaperAlert(
|
220 |
+
"Error in Updation",
|
221 |
+
"Profile Updation Status",
|
222 |
+
"fa-exclamation-circle",
|
223 |
+
4000
|
224 |
+
);
|
225 |
+
}
|
226 |
+
});
|
227 |
+
|
228 |
+
// Logout functionality
|
229 |
+
logoutBtn.addEventListener("click", async () => {
|
230 |
+
try {
|
231 |
+
await auth.signOut();
|
232 |
+
localStorage.clear();
|
233 |
+
window.location.href = "/home";
|
234 |
+
} catch (error) {
|
235 |
+
console.error("Logout error:", error);
|
236 |
+
}
|
237 |
+
});
|
238 |
+
});
|
239 |
+
// Paper animation class
|
240 |
+
class Paper {
|
241 |
+
constructor(x, y) {
|
242 |
+
this.x = x;
|
243 |
+
this.y = y;
|
244 |
+
this.size = Math.random() * 15 + 5;
|
245 |
+
this.rotation = Math.random() * 360;
|
246 |
+
this.speedX = Math.random() * 3 - 1.5;
|
247 |
+
this.speedY = Math.random() * 3 - 1.5;
|
248 |
+
this.rotationSpeed = Math.random() * 2 - 1;
|
249 |
+
this.opacity = 1;
|
250 |
+
this.life = 100;
|
251 |
+
this.element = document.createElement('div');
|
252 |
+
this.element.className = 'paper';
|
253 |
+
this.element.style.width = `${this.size}px`;
|
254 |
+
this.element.style.height = `${this.size}px`;
|
255 |
+
this.element.style.left = `${this.x}px`;
|
256 |
+
this.element.style.top = `${this.y}px`;
|
257 |
+
this.element.style.transform = `rotate(${this.rotation}deg)`;
|
258 |
+
this.element.style.opacity = this.opacity;
|
259 |
+
document.body.appendChild(this.element);
|
260 |
+
}
|
261 |
+
|
262 |
+
update() {
|
263 |
+
this.life--;
|
264 |
+
this.x += this.speedX;
|
265 |
+
this.y += this.speedY;
|
266 |
+
this.rotation += this.rotationSpeed;
|
267 |
+
this.opacity = this.life / 100;
|
268 |
+
|
269 |
+
this.element.style.left = `${this.x}px`;
|
270 |
+
this.element.style.top = `${this.y}px`;
|
271 |
+
this.element.style.transform = `rotate(${this.rotation}deg)`;
|
272 |
+
this.element.style.opacity = this.opacity;
|
273 |
+
|
274 |
+
if (this.life <= 0) {
|
275 |
+
this.element.remove();
|
276 |
+
return false;
|
277 |
+
}
|
278 |
+
return true;
|
279 |
+
}
|
280 |
+
}
|
281 |
+
|
282 |
+
// Paper animation
|
283 |
+
let papers = [];
|
284 |
+
let frameCount = 0;
|
285 |
+
|
286 |
+
document.addEventListener('mousemove', (e) => {
|
287 |
+
if (frameCount % 2 === 0) {
|
288 |
+
papers.push(new Paper(e.clientX, e.clientY));
|
289 |
+
}
|
290 |
+
});
|
291 |
+
|
292 |
+
function animate() {
|
293 |
+
frameCount++;
|
294 |
+
papers = papers.filter(paper => paper.update());
|
295 |
+
requestAnimationFrame(animate);
|
296 |
+
}
|
297 |
+
|
298 |
+
animate();
|
299 |
+
|
300 |
+
// Beam connections animation
|
301 |
+
document.addEventListener('DOMContentLoaded', () => {
|
302 |
+
// Get references to all circles
|
303 |
+
const container = document.getElementById('beam-container');
|
304 |
+
const circle1 = document.getElementById('circle1');
|
305 |
+
const circle2 = document.getElementById('circle2');
|
306 |
+
const circle3 = document.getElementById('circle3');
|
307 |
+
const circle4 = document.getElementById('circle4');
|
308 |
+
const circle5 = document.getElementById('circle5');
|
309 |
+
const circle6 = document.getElementById('circle6');
|
310 |
+
const circle7 = document.getElementById('circle7');
|
311 |
+
|
312 |
+
// Get SVG container
|
313 |
+
const svgContainer = document.getElementById('beams-svg');
|
314 |
+
|
315 |
+
// Create beam connections
|
316 |
+
createBeam(circle1, circle6);
|
317 |
+
createBeam(circle2, circle6);
|
318 |
+
createBeam(circle3, circle6);
|
319 |
+
createBeam(circle4, circle6);
|
320 |
+
createBeam(circle5, circle6);
|
321 |
+
createBeam(circle6, circle7);
|
322 |
+
|
323 |
+
// Handle window resize
|
324 |
+
window.addEventListener('resize', () => {
|
325 |
+
// Clear existing beams
|
326 |
+
svgContainer.innerHTML = '';
|
327 |
+
|
328 |
+
// Recreate beams
|
329 |
+
createBeam(circle1, circle6);
|
330 |
+
createBeam(circle2, circle6);
|
331 |
+
createBeam(circle3, circle6);
|
332 |
+
createBeam(circle4, circle6);
|
333 |
+
createBeam(circle5, circle6);
|
334 |
+
createBeam(circle6, circle7);
|
335 |
+
});
|
336 |
+
|
337 |
+
// Function to create an animated beam between two elements
|
338 |
+
function createBeam(fromElement, toElement) {
|
339 |
+
// Get positions
|
340 |
+
const fromRect = fromElement.getBoundingClientRect();
|
341 |
+
const toRect = toElement.getBoundingClientRect();
|
342 |
+
const containerRect = container.getBoundingClientRect();
|
343 |
+
|
344 |
+
// Calculate center points relative to the container
|
345 |
+
const fromX = fromRect.left + fromRect.width / 2 - containerRect.left;
|
346 |
+
const fromY = fromRect.top + fromRect.height / 2 - containerRect.top;
|
347 |
+
const toX = toRect.left + toRect.width / 2 - containerRect.left;
|
348 |
+
const toY = toRect.top + toRect.height / 2 - containerRect.top;
|
349 |
+
|
350 |
+
// Create the base beam line
|
351 |
+
const beam = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
352 |
+
beam.setAttribute('x1', fromX);
|
353 |
+
beam.setAttribute('y1', fromY);
|
354 |
+
beam.setAttribute('x2', toX);
|
355 |
+
beam.setAttribute('y2', toY);
|
356 |
+
beam.classList.add('beam');
|
357 |
+
svgContainer.appendChild(beam);
|
358 |
+
|
359 |
+
// Create the glowing beam
|
360 |
+
const beamGlow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
361 |
+
beamGlow.setAttribute('x1', fromX);
|
362 |
+
beamGlow.setAttribute('y1', fromY);
|
363 |
+
beamGlow.setAttribute('x2', toX);
|
364 |
+
beamGlow.setAttribute('y2', toY);
|
365 |
+
beamGlow.classList.add('beam-glow');
|
366 |
+
svgContainer.appendChild(beamGlow);
|
367 |
+
}
|
368 |
+
});
|
369 |
+
|
370 |
+
// Data flow animation
|
371 |
+
const inputFiles = [
|
372 |
+
{ y: 120, ext: 'ieee' },
|
373 |
+
{ y: 160, ext: 'arxiv' },
|
374 |
+
{ y: 200, ext: 'PMC' },
|
375 |
+
{ y: 240, ext: 'jstor' },
|
376 |
+
{ y: 280, ext: 'plos' }
|
377 |
+
];
|
378 |
+
|
379 |
+
const outputFiles = [
|
380 |
+
{ label: 'Trend Analysis', y: 100, logoSrc: '/assets/c1.png' },
|
381 |
+
{ label: 'Citation Network', y: 180, logoSrc: '/assets/c2.png' },
|
382 |
+
{ label: 'Venue & Publisher', y: 260, logoSrc: '/assets/c3.png' },
|
383 |
+
];
|
384 |
+
|
385 |
+
const svgContainer = document.querySelector('.paths-container');
|
386 |
+
const centerBox = document.querySelector('.center-box');
|
387 |
+
|
388 |
+
// Animation state
|
389 |
+
let isAnimating = false;
|
390 |
+
|
391 |
+
function createInputPath(startY) {
|
392 |
+
const startX = 100;
|
393 |
+
const endX = 400;
|
394 |
+
const endY = 200;
|
395 |
+
const controlX = (startX + endX) / 2;
|
396 |
+
return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
|
397 |
+
}
|
398 |
+
|
399 |
+
function createOutputPath(endY) {
|
400 |
+
const startX = 400;
|
401 |
+
const startY = 200;
|
402 |
+
const endX = 680;
|
403 |
+
const controlX = (startX + endX) / 2;
|
404 |
+
return `M ${startX} ${startY} C ${controlX} ${startY}, ${controlX} ${endY}, ${endX} ${endY}`;
|
405 |
+
}
|
406 |
+
|
407 |
+
function createInputPaper(data) {
|
408 |
+
const container = document.createElement('div');
|
409 |
+
container.className = 'paper-container';
|
410 |
+
|
411 |
+
const paperIcon = document.createElement('div');
|
412 |
+
paperIcon.className = 'paper-icon';
|
413 |
+
paperIcon.textContent = data.ext;
|
414 |
+
|
415 |
+
const label = document.createElement('span');
|
416 |
+
label.textContent = data.label || data.ext;
|
417 |
+
|
418 |
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
419 |
+
path.setAttribute('class', 'path');
|
420 |
+
path.setAttribute('d', createInputPath(data.y));
|
421 |
+
svgContainer.appendChild(path);
|
422 |
+
|
423 |
+
container.appendChild(paperIcon);
|
424 |
+
container.appendChild(label);
|
425 |
+
document.querySelector('.container').appendChild(container);
|
426 |
+
|
427 |
+
container.style.left = '70px';
|
428 |
+
container.style.top = `${data.y}px`;
|
429 |
+
|
430 |
+
return { container, paperIcon, path, ext: data.ext };
|
431 |
+
}
|
432 |
+
|
433 |
+
function createOutputBox(data) {
|
434 |
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
435 |
+
path.setAttribute('class', 'path');
|
436 |
+
path.setAttribute('d', createOutputPath(data.y));
|
437 |
+
svgContainer.appendChild(path);
|
438 |
+
|
439 |
+
// Create output box
|
440 |
+
const outputBox = document.createElement('div');
|
441 |
+
outputBox.className = 'output-box';
|
442 |
+
document.querySelector('.container').appendChild(outputBox);
|
443 |
+
outputBox.style.top = `${data.y}px`;
|
444 |
+
|
445 |
+
// Create logo image element
|
446 |
+
const logo = document.createElement('img');
|
447 |
+
logo.className = 'output-logo';
|
448 |
+
logo.src = data.logoSrc;
|
449 |
+
logo.alt = data.label + ' logo';
|
450 |
+
outputBox.appendChild(logo);
|
451 |
+
|
452 |
+
// Add title below the box
|
453 |
+
const title = document.createElement('div');
|
454 |
+
title.className = 'box-title';
|
455 |
+
title.textContent = data.label;
|
456 |
+
outputBox.appendChild(title);
|
457 |
+
|
458 |
+
return { path, outputBox };
|
459 |
+
}
|
460 |
+
|
461 |
+
function animatePaperAlongPath(path, ext, onComplete) {
|
462 |
+
const movingPaper = document.createElement('div');
|
463 |
+
movingPaper.className = 'moving-paper';
|
464 |
+
movingPaper.textContent = ext;
|
465 |
+
document.querySelector('.container').appendChild(movingPaper);
|
466 |
+
|
467 |
+
const length = path.getTotalLength();
|
468 |
+
let start = null;
|
469 |
+
const duration = 1200;
|
470 |
+
|
471 |
+
path.style.strokeOpacity = "1"; // Make line visible when animation starts
|
472 |
+
path.style.strokeDasharray = `${length}, ${length}`;
|
473 |
+
path.style.strokeDashoffset = length;
|
474 |
+
|
475 |
+
function animate(timestamp) {
|
476 |
+
if (!start) start = timestamp;
|
477 |
+
const progress = (timestamp - start) / duration;
|
478 |
+
|
479 |
+
if (progress <= 1) {
|
480 |
+
const point = path.getPointAtLength(length * progress);
|
481 |
+
movingPaper.style.left = `${point.x}px`;
|
482 |
+
movingPaper.style.top = `${point.y}px`;
|
483 |
+
|
484 |
+
// Reveal only the traveled portion of the path
|
485 |
+
path.style.strokeDashoffset = length * (1 - progress);
|
486 |
+
|
487 |
+
requestAnimationFrame(animate);
|
488 |
+
} else {
|
489 |
+
movingPaper.remove();
|
490 |
+
path.style.strokeOpacity = "0"; // Hide line when animation completes
|
491 |
+
if (onComplete) onComplete();
|
492 |
+
}
|
493 |
+
}
|
494 |
+
|
495 |
+
requestAnimationFrame(animate);
|
496 |
+
}
|
497 |
+
|
498 |
+
|
499 |
+
function animateDotAlongPath(path, outputBox, onComplete) {
|
500 |
+
const movingDot = document.createElement('div');
|
501 |
+
movingDot.className = 'moving-dot';
|
502 |
+
document.querySelector('.container').appendChild(movingDot);
|
503 |
+
|
504 |
+
const length = path.getTotalLength();
|
505 |
+
let start = null;
|
506 |
+
const duration = 1000;
|
507 |
+
|
508 |
+
path.style.strokeOpacity = "1"; // Make line visible when animation starts
|
509 |
+
path.style.strokeDasharray = `${length}, ${length}`;
|
510 |
+
path.style.strokeDashoffset = length;
|
511 |
+
|
512 |
+
function animate(timestamp) {
|
513 |
+
if (!start) start = timestamp;
|
514 |
+
const progress = (timestamp - start) / duration;
|
515 |
+
|
516 |
+
if (progress <= 1) {
|
517 |
+
const point = path.getPointAtLength(length * progress);
|
518 |
+
movingDot.style.left = `${point.x}px`;
|
519 |
+
movingDot.style.top = `${point.y}px`;
|
520 |
+
|
521 |
+
// Reveal only the traveled portion of the path
|
522 |
+
path.style.strokeDashoffset = length * (1 - progress);
|
523 |
+
|
524 |
+
requestAnimationFrame(animate);
|
525 |
+
} else {
|
526 |
+
movingDot.remove();
|
527 |
+
path.style.strokeOpacity = "0"; // Hide line when animation completes
|
528 |
+
|
529 |
+
outputBox.style.display = 'flex'; // Show output box
|
530 |
+
outputBox.classList.add('highlight');
|
531 |
+
setTimeout(() => outputBox.classList.remove('highlight'), 500);
|
532 |
+
|
533 |
+
if (onComplete) onComplete();
|
534 |
+
}
|
535 |
+
}
|
536 |
+
|
537 |
+
requestAnimationFrame(animate);
|
538 |
+
}
|
539 |
+
|
540 |
+
const inputs = inputFiles.map(createInputPaper);
|
541 |
+
const outputs = outputFiles.map(createOutputBox);
|
542 |
+
|
543 |
+
function pulseBox() {
|
544 |
+
centerBox.classList.add('pulse');
|
545 |
+
setTimeout(() => centerBox.classList.remove('pulse'), 300);
|
546 |
+
}
|
547 |
+
|
548 |
+
async function animateInputs() {
|
549 |
+
// Hide output boxes when input animation starts
|
550 |
+
outputs.forEach(output => {
|
551 |
+
output.outputBox.style.opacity = '0';
|
552 |
+
});
|
553 |
+
|
554 |
+
// Animate all inputs (papers) to the center sequentially
|
555 |
+
for (let i = 0; i < inputs.length; i++) {
|
556 |
+
await new Promise(resolve => {
|
557 |
+
animatePaperAlongPath(inputs[i].path, inputs[i].ext, () => {
|
558 |
+
pulseBox();
|
559 |
+
resolve();
|
560 |
+
});
|
561 |
+
});
|
562 |
+
// Small delay between input animations
|
563 |
+
await new Promise(resolve => setTimeout(resolve, 200));
|
564 |
+
}
|
565 |
+
|
566 |
+
pulseBox(); // Final pulse after all inputs are processed
|
567 |
+
return true;
|
568 |
+
}
|
569 |
+
|
570 |
+
async function animateOutputs() {
|
571 |
+
// Animate all outputs sequentially
|
572 |
+
for (let i = 0; i < outputs.length; i++) {
|
573 |
+
await new Promise(resolve => {
|
574 |
+
animateDotAlongPath(outputs[i].path, outputs[i].outputBox, () => {
|
575 |
+
outputs[i].outputBox.style.opacity = '1'; // Show output box when dot reaches destination
|
576 |
+
resolve();
|
577 |
+
});
|
578 |
+
});
|
579 |
+
|
580 |
+
// Wait a bit before starting the next output animation
|
581 |
+
await new Promise(resolve => setTimeout(resolve, 300));
|
582 |
+
}
|
583 |
+
|
584 |
+
// Keep outputs visible for a moment
|
585 |
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
586 |
+
|
587 |
+
// Before next cycle, fade out output boxes
|
588 |
+
outputs.forEach(output => {
|
589 |
+
output.outputBox.style.opacity = '0';
|
590 |
+
});
|
591 |
+
|
592 |
+
// Small pause before starting next cycle
|
593 |
+
await new Promise(resolve => setTimeout(resolve, 500));
|
594 |
+
return true;
|
595 |
+
}
|
596 |
+
|
597 |
+
async function completeAnimationCycle() {
|
598 |
+
if (isAnimating) return;
|
599 |
+
isAnimating = true;
|
600 |
+
|
601 |
+
try {
|
602 |
+
await animateInputs();
|
603 |
+
await new Promise(resolve => setTimeout(resolve, 800));
|
604 |
+
await animateOutputs();
|
605 |
+
isAnimating = false;
|
606 |
+
setTimeout(completeAnimationCycle, 500);
|
607 |
+
} catch (error) {
|
608 |
+
console.error("Animation error:", error);
|
609 |
+
isAnimating = false;
|
610 |
+
}
|
611 |
+
}
|
612 |
+
|
613 |
+
setTimeout(completeAnimationCycle, 1000);
|
614 |
+
|
615 |
+
function showPaperAlert(message, title = "Alert", icon = "fa-exclamation-circle", duration = 7000) {
|
616 |
+
const paperAlert = document.getElementById('paperAlert');
|
617 |
+
const alertMessage = document.getElementById('alertMessage');
|
618 |
+
const alertTitle = document.getElementById('alertTitle');
|
619 |
+
const alertIcon = document.getElementById('alertIcon');
|
620 |
+
|
621 |
+
// Set custom message, title and icon
|
622 |
+
alertMessage.textContent = message;
|
623 |
+
alertTitle.textContent = title;
|
624 |
+
|
625 |
+
// Update the icon class
|
626 |
+
alertIcon.className = ''; // Clear existing classes
|
627 |
+
alertIcon.classList.add('fas', icon);
|
628 |
+
|
629 |
+
// Show the alert
|
630 |
+
paperAlert.classList.remove("hide");
|
631 |
+
paperAlert.classList.add("show", "showAlert");
|
632 |
+
|
633 |
+
// Auto-hide after specified duration
|
634 |
+
setTimeout(() => {
|
635 |
+
hideAlert();
|
636 |
+
}, duration);
|
637 |
+
}
|
638 |
+
|
639 |
+
// Function to hide the alert with animation
|
640 |
+
function hideAlert() {
|
641 |
+
const paperAlert = document.getElementById('paperAlert');
|
642 |
+
paperAlert.classList.remove("show");
|
643 |
+
paperAlert.classList.add("hide");
|
644 |
+
setTimeout(() => {
|
645 |
+
paperAlert.classList.remove("showAlert");
|
646 |
+
}, 500);
|
647 |
+
}
|
648 |
+
|
649 |
+
// Set up close button functionality
|
650 |
+
document.querySelector('#paperAlert .close-btn').addEventListener('click', function() {
|
651 |
+
hideAlert();
|
652 |
+
});
|
653 |
+
|
654 |
+
const featureCards = document.querySelectorAll('.feature-selection .glass');
|
655 |
+
const selectionMessage = document.getElementById('selectionMessage');
|
656 |
+
const selectedFeatureText = document.getElementById('selectedFeature');
|
657 |
+
const selectedFeature = {"feature":null}
|
658 |
+
|
659 |
+
let isLoggedIn = localStorage.getItem("username"); // Replace with your actual login check
|
660 |
+
let selectedCard = null;
|
661 |
+
|
662 |
+
featureCards.forEach(card => {
|
663 |
+
card.addEventListener('click', () => {
|
664 |
+
if (!isLoggedIn) {
|
665 |
+
|
666 |
+
showPaperAlert(
|
667 |
+
"You must be Logged In to continue!",
|
668 |
+
"Authentication Required",
|
669 |
+
"fa-exclamation-circle",
|
670 |
+
7000
|
671 |
+
);
|
672 |
+
return;
|
673 |
+
}
|
674 |
+
const isAlreadySelected = card === selectedCard;
|
675 |
+
|
676 |
+
if (isAlreadySelected) {
|
677 |
+
card.classList.remove('selected');
|
678 |
+
selectedCard = null;
|
679 |
+
selectionMessage.classList.remove('visible');
|
680 |
+
} else {
|
681 |
+
if (selectedCard) {
|
682 |
+
selectedCard.classList.remove('selected');
|
683 |
+
}
|
684 |
+
|
685 |
+
card.classList.add('selected');
|
686 |
+
selectedCard = card;
|
687 |
+
|
688 |
+
selectedFeature.feature = card.getAttribute('data-feature');
|
689 |
+
|
690 |
+
selectedFeatureText.textContent = card.getAttribute('data-text');
|
691 |
+
selectionMessage.classList.add('visible');
|
692 |
+
}
|
693 |
+
});
|
694 |
+
});
|
695 |
+
|
696 |
+
|
697 |
+
|
698 |
+
const searchBtn = document.getElementById("searchbtn");
|
699 |
+
searchBtn.addEventListener("click", function () {
|
700 |
+
console.log("Search button clicked");
|
701 |
+
|
702 |
+
|
703 |
+
|
704 |
+
const isLoggedIn = localStorage.getItem("username");
|
705 |
+
if (!isLoggedIn) {
|
706 |
+
alert("You must be signed in to perform a search.");
|
707 |
+
return;
|
708 |
+
}
|
709 |
+
|
710 |
+
const searchInput = document.querySelector('#searchy');
|
711 |
+
console.log("searchInput" , searchInput)
|
712 |
+
if (!searchInput) {
|
713 |
+
console.error("Search input element not found");
|
714 |
+
alert("Search input not found. Please check the page structure.");
|
715 |
+
return;
|
716 |
+
}
|
717 |
+
|
718 |
+
const searchTopic = searchInput.value.trim();
|
719 |
+
const yearFilter = document.getElementById('year-filter');
|
720 |
+
const searchYear = yearFilter ? yearFilter.value : "";
|
721 |
+
const userId = localStorage.getItem("userId");
|
722 |
+
|
723 |
+
saveSearchToFirebase(searchTopic, searchYear, selectedFeature )
|
724 |
+
// alert(" saved the activity")
|
725 |
+
|
726 |
+
// Validate inputs
|
727 |
+
if (!searchTopic) {
|
728 |
+
alert("Please enter a search topic");
|
729 |
+
return;
|
730 |
+
}
|
731 |
+
|
732 |
+
console.log(`Searching for: ${searchTopic}, Year: ${searchYear}, Feature: ${selectedFeature}`);
|
733 |
+
|
734 |
+
|
735 |
+
let currentPage = 0;
|
736 |
+
const nextBtn = document.getElementById("nextBtn");
|
737 |
+
|
738 |
+
nextBtn.addEventListener("click", function () {
|
739 |
+
currentPage+=1
|
740 |
+
alert("just checking bro " +currentPage );
|
741 |
+
trendanalysis(currentPage);
|
742 |
+
});
|
743 |
+
|
744 |
+
|
745 |
+
|
746 |
+
function trendanalysis(pageNo){
|
747 |
+
const originalText = searchBtn.textContent;
|
748 |
+
searchBtn.textContent = "Processing...";
|
749 |
+
searchBtn.disabled = true;
|
750 |
+
nextBtn.style.display = "inline-block";
|
751 |
+
|
752 |
+
|
753 |
+
fetch('/check-data-exists/', {
|
754 |
+
method: 'POST',
|
755 |
+
headers: {
|
756 |
+
'Content-Type': 'application/json',
|
757 |
+
},
|
758 |
+
body: JSON.stringify({
|
759 |
+
userId: userId,
|
760 |
+
topic: searchTopic,
|
761 |
+
year: searchYear
|
762 |
+
})
|
763 |
+
})
|
764 |
+
.then(response => {
|
765 |
+
if (!response.ok) {
|
766 |
+
throw new Error(`Network response was not ok: ${response.status}`);
|
767 |
+
}
|
768 |
+
return response.json();
|
769 |
+
})
|
770 |
+
.then(data => {
|
771 |
+
if (data.exists) {
|
772 |
+
console.log('Data exists, skipping analysis step');
|
773 |
+
showPaperAlert(
|
774 |
+
"Data exists",
|
775 |
+
"Trend Analysis.......",
|
776 |
+
"fa-exclamation-circle",
|
777 |
+
7000
|
778 |
+
);
|
779 |
+
return fetch('/analyze-trends/', {
|
780 |
+
method: 'POST',
|
781 |
+
headers: {
|
782 |
+
'Content-Type': 'application/json',
|
783 |
+
},
|
784 |
+
body: JSON.stringify({
|
785 |
+
userId: userId,
|
786 |
+
topic: searchTopic,
|
787 |
+
year: searchYear,
|
788 |
+
page: pageNo
|
789 |
+
})
|
790 |
+
});
|
791 |
+
} else {
|
792 |
+
console.log('Data does not exist, starting full analysis');
|
793 |
+
showPaperAlert(
|
794 |
+
"Starting full analysis",
|
795 |
+
"ATrend Analysis.......",
|
796 |
+
"fa-exclamation-circle",
|
797 |
+
7000
|
798 |
+
);
|
799 |
+
return fetch('/analyze/', {
|
800 |
+
method: 'POST',
|
801 |
+
headers: {
|
802 |
+
'Content-Type': 'application/json',
|
803 |
+
},
|
804 |
+
body: JSON.stringify({
|
805 |
+
userId: userId,
|
806 |
+
topic: searchTopic,
|
807 |
+
year: searchYear
|
808 |
+
})
|
809 |
+
})
|
810 |
+
.then(response => {
|
811 |
+
if (!response.ok) {
|
812 |
+
throw new Error(`Analysis failed: ${response.status}`);
|
813 |
+
}
|
814 |
+
return response.json();
|
815 |
+
})
|
816 |
+
.then(analysisData => {
|
817 |
+
console.log('Analysis complete:', analysisData);
|
818 |
+
// Now proceed to analyze-trends
|
819 |
+
showPaperAlert(
|
820 |
+
"Analysis complete",
|
821 |
+
"Trend Analysis.......",
|
822 |
+
"fa-exclamation-circle",
|
823 |
+
7000
|
824 |
+
);
|
825 |
+
return fetch('/analyze-trends/', {
|
826 |
+
method: 'POST',
|
827 |
+
headers: {
|
828 |
+
'Content-Type': 'application/json',
|
829 |
+
},
|
830 |
+
body: JSON.stringify({
|
831 |
+
userId: userId,
|
832 |
+
topic: searchTopic,
|
833 |
+
year: searchYear,
|
834 |
+
page: pageNo
|
835 |
+
})
|
836 |
+
});
|
837 |
+
});
|
838 |
+
}
|
839 |
+
})
|
840 |
+
.then(response => {
|
841 |
+
if (!response.ok) {
|
842 |
+
throw new Error(`Trend analysis failed: ${response.status}`);
|
843 |
+
}
|
844 |
+
return response.json();
|
845 |
+
})
|
846 |
+
.then(trendData => {
|
847 |
+
console.log('Trend analysis complete:', trendData);
|
848 |
+
showPaperAlert(
|
849 |
+
"Analysis complete",
|
850 |
+
"Trend Analysis.......",
|
851 |
+
"fa-exclamation-circle",
|
852 |
+
7000
|
853 |
+
);
|
854 |
+
loadingOverlay.hide()
|
855 |
+
// Handle redirection to results page or display results
|
856 |
+
})
|
857 |
+
.catch(error => {
|
858 |
+
console.error('Error:', error);
|
859 |
+
alert("There was an error processing your request. Please try again: " + error.message);
|
860 |
+
showPaperAlert(
|
861 |
+
`There was an error processing your request. Please try again:` + error.message,
|
862 |
+
"Trend Analysis.......",
|
863 |
+
"fa-exclamation-circle",
|
864 |
+
7000
|
865 |
+
);
|
866 |
+
loadingOverlay.hide()
|
867 |
+
})
|
868 |
+
.finally(() => {
|
869 |
+
// Always reset button state
|
870 |
+
searchBtn.textContent = originalText;
|
871 |
+
searchBtn.disabled = false;
|
872 |
+
});
|
873 |
+
}
|
874 |
+
|
875 |
+
|
876 |
+
|
877 |
+
if (selectedFeature.feature === 'trends') {
|
878 |
+
console.log("here here")
|
879 |
+
loadingOverlay.show("Loading Trend Analysis")
|
880 |
+
trendanalysis(0);
|
881 |
+
}
|
882 |
+
else if (selectedFeature.feature === 'citations') {
|
883 |
+
loadingOverlay.show("Loading Citation Network Analysis")
|
884 |
+
nextBtn.style.display = "none";
|
885 |
+
currentPage = 0;
|
886 |
+
showPaperAlert(
|
887 |
+
"Analysis Started",
|
888 |
+
"Citation Network Analysis.......",
|
889 |
+
"fa-exclamation-circle",
|
890 |
+
7000
|
891 |
+
);
|
892 |
+
|
893 |
+
const originalText = this.textContent;
|
894 |
+
this.textContent = "Processing...";
|
895 |
+
this.disabled = true;
|
896 |
+
|
897 |
+
// First check if data exists in MongoDB
|
898 |
+
fetch('/check-data-exists-citation/', {
|
899 |
+
method: 'POST',
|
900 |
+
headers: {
|
901 |
+
'Content-Type': 'application/json',
|
902 |
+
},
|
903 |
+
body: JSON.stringify({
|
904 |
+
userId: userId,
|
905 |
+
topic: searchTopic,
|
906 |
+
year: searchYear
|
907 |
+
})
|
908 |
+
})
|
909 |
+
.then(response => {
|
910 |
+
if (!response.ok) {
|
911 |
+
throw new Error(`Network response was not ok: ${response.status}`);
|
912 |
+
}
|
913 |
+
return response.json();
|
914 |
+
})
|
915 |
+
.then(data => {
|
916 |
+
if (data.exists) {
|
917 |
+
console.log('Data exists, fetching citation data...');
|
918 |
+
loadingOverlay.hide()
|
919 |
+
window.open(`/citation-data?userId=${encodeURIComponent(userId)}&topic=${encodeURIComponent(searchTopic)}&year=${encodeURIComponent(searchYear)}`, '_blank');
|
920 |
+
} else {
|
921 |
+
console.log('Data does not exist, starting full analysis...');
|
922 |
+
showPaperAlert(
|
923 |
+
"Data Collecting....",
|
924 |
+
"Citation Network Analysis.......",
|
925 |
+
"fa-exclamation-circle",
|
926 |
+
7000
|
927 |
+
);
|
928 |
+
return fetch('/save', {
|
929 |
+
method: 'POST',
|
930 |
+
headers: {
|
931 |
+
'Content-Type': 'application/json',
|
932 |
+
},
|
933 |
+
body: JSON.stringify({
|
934 |
+
userId: userId,
|
935 |
+
topic: searchTopic,
|
936 |
+
year: searchYear
|
937 |
+
})
|
938 |
+
})
|
939 |
+
.then(response => {
|
940 |
+
if (!response.ok) {
|
941 |
+
throw new Error(`Save failed: ${response.status}`);
|
942 |
+
}
|
943 |
+
return response.json();
|
944 |
+
})
|
945 |
+
.then(saveData => {
|
946 |
+
console.log('Data saved successfully:', saveData);
|
947 |
+
loadingOverlay.hide()
|
948 |
+
window.open(`/citation-data?userId=${encodeURIComponent(userId)}&topic=${encodeURIComponent(searchTopic)}&year=${encodeURIComponent(searchYear)}`, '_blank');
|
949 |
+
});
|
950 |
+
}
|
951 |
+
})
|
952 |
+
.catch(error => {
|
953 |
+
console.error('Error:', error);
|
954 |
+
showPaperAlert(
|
955 |
+
"There was an error processing your request. Please try again: " + error.message,
|
956 |
+
"Citation Network Analysis.......",
|
957 |
+
"fa-exclamation-circle",
|
958 |
+
7000
|
959 |
+
);
|
960 |
+
loadingOverlay.hide()
|
961 |
+
})
|
962 |
+
.finally(() => {
|
963 |
+
// Always reset button state
|
964 |
+
this.textContent = originalText;
|
965 |
+
this.disabled = false;
|
966 |
+
});
|
967 |
+
}
|
968 |
+
|
969 |
+
else if (selectedFeature.feature === 'venues') {
|
970 |
+
nextBtn.style.display = "none";
|
971 |
+
currentPage = 0;
|
972 |
+
const originalText = this.textContent;
|
973 |
+
this.textContent = "Processing...";
|
974 |
+
this.disabled = true;
|
975 |
+
console.log(" here here venue")
|
976 |
+
showPaperAlert(
|
977 |
+
"Analysis Started",
|
978 |
+
"Venue/Publisher Analysis.......",
|
979 |
+
"fa-exclamation-circle",
|
980 |
+
7000
|
981 |
+
);
|
982 |
+
loadingOverlay.show("Started Loading.....")
|
983 |
+
fetch('/check-data-exists-venue/', {
|
984 |
+
method: 'POST',
|
985 |
+
headers: {
|
986 |
+
'Content-Type': 'application/json',
|
987 |
+
},
|
988 |
+
body: JSON.stringify({
|
989 |
+
userId: userId,
|
990 |
+
topic: searchTopic,
|
991 |
+
year: searchYear
|
992 |
+
})
|
993 |
+
})
|
994 |
+
.then(response => {
|
995 |
+
if (!response.ok) {
|
996 |
+
|
997 |
+
throw new Error(`Network response was not ok: ${response.status}`);
|
998 |
+
|
999 |
+
}
|
1000 |
+
|
1001 |
+
return response.json();
|
1002 |
+
})
|
1003 |
+
.then(data => {
|
1004 |
+
if (data.exists) {
|
1005 |
+
console.log('Data exists, loading dashboard directly');
|
1006 |
+
// If data exists, go directly to load_and_display_dashboard
|
1007 |
+
|
1008 |
+
return fetch('/load_and_display_dashboard/', {
|
1009 |
+
method: 'POST',
|
1010 |
+
headers: {
|
1011 |
+
'Content-Type': 'application/json',
|
1012 |
+
},
|
1013 |
+
body: JSON.stringify({
|
1014 |
+
userId: userId,
|
1015 |
+
topic: searchTopic,
|
1016 |
+
year: searchYear
|
1017 |
+
})
|
1018 |
+
});
|
1019 |
+
} else {
|
1020 |
+
console.log('Data does not exist, starting search process');
|
1021 |
+
showPaperAlert(
|
1022 |
+
"Data Collectiong....",
|
1023 |
+
"Venue/Publisher Analysis.......",
|
1024 |
+
"fa-exclamation-circle",
|
1025 |
+
7000
|
1026 |
+
);
|
1027 |
+
return fetch('/search/', {
|
1028 |
+
method: 'POST',
|
1029 |
+
headers: {
|
1030 |
+
'Content-Type': 'application/json',
|
1031 |
+
},
|
1032 |
+
body: JSON.stringify({
|
1033 |
+
userId: userId,
|
1034 |
+
topic: searchTopic,
|
1035 |
+
year: searchYear
|
1036 |
+
})
|
1037 |
+
})
|
1038 |
+
.then(response => {
|
1039 |
+
if (!response.ok) {
|
1040 |
+
throw new Error(`Search failed: ${response.status}`);
|
1041 |
+
}
|
1042 |
+
return response.json();
|
1043 |
+
})
|
1044 |
+
.then(searchData => {
|
1045 |
+
console.log('Search complete:', searchData);
|
1046 |
+
|
1047 |
+
return fetch('/load_and_display_dashboard/', {
|
1048 |
+
method: 'POST',
|
1049 |
+
headers: {
|
1050 |
+
'Content-Type': 'application/json',
|
1051 |
+
},
|
1052 |
+
body: JSON.stringify({
|
1053 |
+
userId: userId,
|
1054 |
+
topic: searchTopic,
|
1055 |
+
year: searchYear
|
1056 |
+
})
|
1057 |
+
});
|
1058 |
+
});
|
1059 |
+
}
|
1060 |
+
})
|
1061 |
+
.then(response => {
|
1062 |
+
if (!response.ok) {
|
1063 |
+
throw new Error(`Dashboard loading failed: ${response.status}`);
|
1064 |
+
}
|
1065 |
+
return response.json();
|
1066 |
+
})
|
1067 |
+
.then(dashboardData => {
|
1068 |
+
|
1069 |
+
console.log('Dashboard loaded successfully:', dashboardData);
|
1070 |
+
loadingOverlay.hide();
|
1071 |
+
//
|
1072 |
+
})
|
1073 |
+
.catch(error => {
|
1074 |
+
console.error('Error:', error);
|
1075 |
+
showPaperAlert(
|
1076 |
+
"There was an error processing your request. Please try again: " + error.message,
|
1077 |
+
"Venue/Publisher Analysis.......",
|
1078 |
+
"fa-exclamation-circle",
|
1079 |
+
7000
|
1080 |
+
);
|
1081 |
+
})
|
1082 |
+
.finally(() => {
|
1083 |
+
this.textContent = originalText;
|
1084 |
+
this.disabled = false;
|
1085 |
+
});
|
1086 |
+
|
1087 |
+
} else {
|
1088 |
+
alert("Invalid feature selected. Please choose a valid feature.");
|
1089 |
+
}
|
1090 |
+
});
|
1091 |
+
|
1092 |
+
|
1093 |
+
const loadingOverlay = {
|
1094 |
+
overlay: document.getElementById('loading-overlay'),
|
1095 |
+
cyclone: null,
|
1096 |
+
loadingText: null,
|
1097 |
+
animationInterval: null,
|
1098 |
+
paperInterval: null,
|
1099 |
+
|
1100 |
+
initialize() {
|
1101 |
+
this.cyclone = document.getElementById('cyclone');
|
1102 |
+
this.loadingText = document.getElementById('loading-text');
|
1103 |
+
this.setupAnimation();
|
1104 |
+
|
1105 |
+
// Make the background whiter by changing the overlay background
|
1106 |
+
this.overlay.style.backgroundColor = 'rgba(137, 138, 183, 0.9)'; // More white, slightly transparent
|
1107 |
+
},
|
1108 |
+
|
1109 |
+
setupAnimation() {
|
1110 |
+
// Pre-setup animation elements so they're ready when needed
|
1111 |
+
for (let i = 0; i < 10; i++) {
|
1112 |
+
this.createPaper(i);
|
1113 |
+
}
|
1114 |
+
},
|
1115 |
+
|
1116 |
+
show(message = 'Processing...') {
|
1117 |
+
this.loadingText.textContent = message;
|
1118 |
+
this.overlay.classList.add('active');
|
1119 |
+
|
1120 |
+
// Start creating papers
|
1121 |
+
this.paperInterval = setInterval(() => {
|
1122 |
+
this.createPaper();
|
1123 |
+
}, 120);
|
1124 |
+
|
1125 |
+
// Animate loading text
|
1126 |
+
let dots = 0;
|
1127 |
+
this.animationInterval = setInterval(() => {
|
1128 |
+
dots = (dots + 1) % 4;
|
1129 |
+
this.loadingText.textContent = message + '.'.repeat(dots);
|
1130 |
+
}, 500);
|
1131 |
+
},
|
1132 |
+
|
1133 |
+
hide() {
|
1134 |
+
this.overlay.classList.remove('active');
|
1135 |
+
|
1136 |
+
// Clear intervals
|
1137 |
+
clearInterval(this.paperInterval);
|
1138 |
+
clearInterval(this.animationInterval);
|
1139 |
+
|
1140 |
+
// Clear all papers after animation ends
|
1141 |
+
setTimeout(() => {
|
1142 |
+
while (this.cyclone.firstChild) {
|
1143 |
+
this.cyclone.removeChild(this.cyclone.firstChild);
|
1144 |
+
}
|
1145 |
+
}, 300);
|
1146 |
+
},
|
1147 |
+
|
1148 |
+
createPaper(index) {
|
1149 |
+
const paper = document.createElement('div');
|
1150 |
+
paper.className = 'paper';
|
1151 |
+
|
1152 |
+
// Randomize paper appearance for more realistic effect
|
1153 |
+
const size = 8 + Math.random() * 12;
|
1154 |
+
paper.style.width = `${size}px`;
|
1155 |
+
paper.style.height = `${size * 1.3}px`;
|
1156 |
+
|
1157 |
+
// Change paper color to grayish (varying shades of gray)
|
1158 |
+
const grayness = 65 + Math.random() * 20; // Range from 65-85% gray (darker)
|
1159 |
+
paper.style.backgroundColor = `hsl(0, 0%, ${grayness}%)`;
|
1160 |
+
|
1161 |
+
// Give it a slight skew to look more like paper
|
1162 |
+
const skewX = Math.random() * 10 - 5;
|
1163 |
+
const skewY = Math.random() * 10 - 5;
|
1164 |
+
paper.style.transform = `skew(${skewX}deg, ${skewY}deg)`;
|
1165 |
+
|
1166 |
+
// Start position - at the top of the cyclone
|
1167 |
+
const startAngle = Math.random() * Math.PI * 2;
|
1168 |
+
const startRadius = 60 + Math.random() * 20;
|
1169 |
+
const startX = 100 + startRadius * Math.cos(startAngle);
|
1170 |
+
const startY = -20; // Start above the visible area
|
1171 |
+
|
1172 |
+
paper.style.left = `${startX}px`;
|
1173 |
+
paper.style.top = `${startY}px`;
|
1174 |
+
|
1175 |
+
this.cyclone.appendChild(paper);
|
1176 |
+
|
1177 |
+
// Animate the paper with a specific starting point in the spiral
|
1178 |
+
this.animatePaperInCyclone(paper, startAngle);
|
1179 |
+
},
|
1180 |
+
|
1181 |
+
animatePaperInCyclone(paper, initialAngle) {
|
1182 |
+
// Animation duration
|
1183 |
+
const duration = 6000 + Math.random() * 2000;
|
1184 |
+
const startTime = Date.now();
|
1185 |
+
|
1186 |
+
// Initial state
|
1187 |
+
let angle = initialAngle || 0;
|
1188 |
+
let height = -20; // Start above the visible area
|
1189 |
+
const maxHeight = 350; // End height
|
1190 |
+
const rotationSpeed = 1.0 + Math.random() * 0.5; // More consistent rotation speed
|
1191 |
+
|
1192 |
+
const animateFrame = () => {
|
1193 |
+
if (!this.overlay.classList.contains('active')) {
|
1194 |
+
if (paper.parentNode === this.cyclone) {
|
1195 |
+
this.cyclone.removeChild(paper);
|
1196 |
+
}
|
1197 |
+
return;
|
1198 |
+
}
|
1199 |
+
|
1200 |
+
const now = Date.now();
|
1201 |
+
const elapsed = now - startTime;
|
1202 |
+
const progress = elapsed / duration;
|
1203 |
+
|
1204 |
+
if (progress < 1) {
|
1205 |
+
// Calculate new height with a clear downward flow
|
1206 |
+
// Move from top to bottom with easing
|
1207 |
+
height = -20 + (maxHeight + 20) * Math.min(1, progress * 1.2);
|
1208 |
+
|
1209 |
+
// Calculate spiral radius - wider at top, narrower at bottom
|
1210 |
+
// Creates a funnel/cyclone shape
|
1211 |
+
const heightProgress = height / maxHeight;
|
1212 |
+
const currentRadius = Math.max(10, 80 - 60 * heightProgress);
|
1213 |
+
|
1214 |
+
// Rotate faster as it gets closer to the bottom
|
1215 |
+
const adjustedRotation = rotationSpeed * (1 + heightProgress * 0.5);
|
1216 |
+
angle += adjustedRotation * 0.03;
|
1217 |
+
|
1218 |
+
// Calculate position
|
1219 |
+
const x = 100 + currentRadius * Math.cos(angle);
|
1220 |
+
const y = height;
|
1221 |
+
|
1222 |
+
// Apply position
|
1223 |
+
paper.style.left = `${x}px`;
|
1224 |
+
paper.style.top = `${y}px`;
|
1225 |
+
|
1226 |
+
// Apply rotation and scaling
|
1227 |
+
const rotation = angle * (180 / Math.PI);
|
1228 |
+
// Increase fluttering effect as paper falls
|
1229 |
+
const flutter = Math.sin(elapsed * 0.01) * (5 + heightProgress * 10);
|
1230 |
+
paper.style.transform = `rotate(${rotation + flutter}deg) scale(${0.8 + Math.sin(progress * Math.PI * 4) * 0.1})`;
|
1231 |
+
|
1232 |
+
// Adjust opacity near the end for a fading effect
|
1233 |
+
if (progress > 0.8) {
|
1234 |
+
paper.style.opacity = (1 - progress) * 5;
|
1235 |
+
}
|
1236 |
+
|
1237 |
+
requestAnimationFrame(animateFrame);
|
1238 |
+
} else {
|
1239 |
+
// Remove paper when animation completes
|
1240 |
+
if (paper.parentNode === this.cyclone) {
|
1241 |
+
this.cyclone.removeChild(paper);
|
1242 |
+
}
|
1243 |
+
}
|
1244 |
+
};
|
1245 |
+
|
1246 |
+
requestAnimationFrame(animateFrame);
|
1247 |
+
}
|
1248 |
+
};
|
1249 |
+
// Initialize the loading overlay when the DOM is loaded
|
1250 |
+
document.addEventListener('DOMContentLoaded', () => {
|
1251 |
+
loadingOverlay.initialize();
|
1252 |
+
});
|
1253 |
+
const words = document.querySelectorAll('.liquid-word');
|
1254 |
+
const splashContainer = document.getElementById('splashContainer');
|
1255 |
+
let currentIndex = 0;
|
1256 |
+
|
1257 |
+
// Prepare each word with character spans and paper drops
|
1258 |
+
words.forEach(word => {
|
1259 |
+
const letters = word.textContent.split('');
|
1260 |
+
word.innerHTML = '';
|
1261 |
+
|
1262 |
+
letters.forEach(letter => {
|
1263 |
+
const charSpan = document.createElement('span');
|
1264 |
+
charSpan.className = 'liquid-char';
|
1265 |
+
charSpan.textContent = letter;
|
1266 |
+
word.appendChild(charSpan);
|
1267 |
+
|
1268 |
+
// Add paper drop to each character
|
1269 |
+
const paperDrop = document.createElement('div');
|
1270 |
+
paperDrop.className = 'paper-drop';
|
1271 |
+
charSpan.appendChild(paperDrop);
|
1272 |
+
|
1273 |
+
// Add paper background to each character
|
1274 |
+
for (let i = 0; i < 3; i++) {
|
1275 |
+
const paperBg = document.createElement('div');
|
1276 |
+
paperBg.className = 'paper-piece';
|
1277 |
+
paperBg.style.width = `${Math.random() * 20 + 20}px`;
|
1278 |
+
paperBg.style.height = `${Math.random() * 15 + 15}px`;
|
1279 |
+
paperBg.style.transform = `rotate(${Math.random() * 40 - 20}deg)`;
|
1280 |
+
|
1281 |
+
// Random position around the character
|
1282 |
+
const offsetX = (Math.random() - 0.5) * 30;
|
1283 |
+
const offsetY = (Math.random() - 0.5) * 30;
|
1284 |
+
paperBg.style.left = `calc(50% + ${offsetX}px)`;
|
1285 |
+
paperBg.style.top = `calc(50% + ${offsetY}px)`;
|
1286 |
+
|
1287 |
+
// Random colors (paper-like)
|
1288 |
+
const colors = ['#ffffff', '#f8f8f8', '#f0f0f0', '#fffafa', '#f5f5f5'];
|
1289 |
+
paperBg.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
1290 |
+
|
1291 |
+
charSpan.appendChild(paperBg);
|
1292 |
+
}
|
1293 |
+
});
|
1294 |
+
});
|
1295 |
+
|
1296 |
+
// Function to create paper splashes
|
1297 |
+
function createPaperSplashes(centerX, centerY) {
|
1298 |
+
for (let i = 0; i < 15; i++) {
|
1299 |
+
setTimeout(() => {
|
1300 |
+
const paper = document.createElement('div');
|
1301 |
+
paper.className = 'paper-piece';
|
1302 |
+
|
1303 |
+
// Random size and shape
|
1304 |
+
const size = Math.random() * 20 + 10;
|
1305 |
+
paper.style.width = `${size}px`;
|
1306 |
+
paper.style.height = `${size * (Math.random() * 0.5 + 0.5)}px`;
|
1307 |
+
|
1308 |
+
// Random rotation
|
1309 |
+
const rotation = Math.random() * 360;
|
1310 |
+
paper.style.setProperty('--rotation', `${rotation}deg`);
|
1311 |
+
|
1312 |
+
// Random position around center
|
1313 |
+
const angle = Math.random() * Math.PI * 2;
|
1314 |
+
const distance = Math.random() * 100 + 20;
|
1315 |
+
const x = centerX + Math.cos(angle) * distance;
|
1316 |
+
const y = centerY + Math.sin(angle) * distance;
|
1317 |
+
|
1318 |
+
paper.style.left = `${x}px`;
|
1319 |
+
paper.style.top = `${y}px`;
|
1320 |
+
|
1321 |
+
// Random paper colors
|
1322 |
+
const colors = ['#ffffff', '#f8f8f8', '#f0f0f0', '#fffafa', '#f5f5f5'];
|
1323 |
+
paper.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
1324 |
+
|
1325 |
+
// Add a slight border on some pieces
|
1326 |
+
if (Math.random() > 0.7) {
|
1327 |
+
paper.style.border = '1px solid rgba(0,0,0,0.05)';
|
1328 |
+
}
|
1329 |
+
|
1330 |
+
// Animation
|
1331 |
+
paper.style.animation = `paperSplash ${Math.random() * 0.8 + 0.6}s forwards`;
|
1332 |
+
|
1333 |
+
splashContainer.appendChild(paper);
|
1334 |
+
|
1335 |
+
// Remove after animation
|
1336 |
+
setTimeout(() => paper.remove(), 1500);
|
1337 |
+
}, i * 40);
|
1338 |
+
}
|
1339 |
+
}
|
1340 |
+
|
1341 |
+
// Function to animate letters in
|
1342 |
+
function animateLettersIn(word) {
|
1343 |
+
const chars = word.querySelectorAll('.liquid-char');
|
1344 |
+
|
1345 |
+
chars.forEach((char, i) => {
|
1346 |
+
setTimeout(() => {
|
1347 |
+
// Animate the character
|
1348 |
+
char.style.animation = 'paperIn 0.7s forwards';
|
1349 |
+
char.style.opacity = '1';
|
1350 |
+
|
1351 |
+
// Animate paper drop
|
1352 |
+
const paperDrop = char.querySelector('.paper-drop');
|
1353 |
+
setTimeout(() => {
|
1354 |
+
paperDrop.style.animation = 'paperDrop 0.5s forwards';
|
1355 |
+
}, 350);
|
1356 |
+
|
1357 |
+
// Animate paper pieces
|
1358 |
+
const paperPieces = char.querySelectorAll('.paper-piece');
|
1359 |
+
paperPieces.forEach((piece, j) => {
|
1360 |
+
setTimeout(() => {
|
1361 |
+
piece.style.opacity = '1';
|
1362 |
+
piece.style.transform += ' scale(1)';
|
1363 |
+
}, j * 100 + 300);
|
1364 |
+
});
|
1365 |
+
|
1366 |
+
}, i * 100);
|
1367 |
+
});
|
1368 |
+
}
|
1369 |
+
|
1370 |
+
// Function to animate letters out
|
1371 |
+
function animateLettersOut(word) {
|
1372 |
+
const chars = word.querySelectorAll('.liquid-char');
|
1373 |
+
|
1374 |
+
chars.forEach((char, i) => {
|
1375 |
+
setTimeout(() => {
|
1376 |
+
char.style.animation = 'paperOut 0.5s forwards';
|
1377 |
+
|
1378 |
+
// Hide paper pieces
|
1379 |
+
const paperPieces = char.querySelectorAll('.paper-piece');
|
1380 |
+
paperPieces.forEach(piece => {
|
1381 |
+
piece.style.opacity = '0';
|
1382 |
+
});
|
1383 |
+
}, i * 60);
|
1384 |
+
});
|
1385 |
+
}
|
1386 |
+
|
1387 |
+
// Function for word transition
|
1388 |
+
function transitionWords() {
|
1389 |
+
// Get current word and calculate center position
|
1390 |
+
const currentWord = words[currentIndex];
|
1391 |
+
const rect = currentWord.getBoundingClientRect();
|
1392 |
+
const centerX = rect.width / 2;
|
1393 |
+
const centerY = rect.height / 2;
|
1394 |
+
|
1395 |
+
// Animate current word out
|
1396 |
+
animateLettersOut(currentWord);
|
1397 |
+
|
1398 |
+
setTimeout(() => {
|
1399 |
+
// Hide current word
|
1400 |
+
currentWord.classList.remove('active');
|
1401 |
+
|
1402 |
+
// Go to next word
|
1403 |
+
currentIndex = (currentIndex + 1) % words.length;
|
1404 |
+
const nextWord = words[currentIndex];
|
1405 |
+
|
1406 |
+
// Show next word
|
1407 |
+
nextWord.classList.add('active');
|
1408 |
+
|
1409 |
+
// Create paper splash effect
|
1410 |
+
createPaperSplashes(centerX, centerY);
|
1411 |
+
|
1412 |
+
// Animate next word in
|
1413 |
+
setTimeout(() => {
|
1414 |
+
animateLettersIn(nextWord);
|
1415 |
+
}, 200);
|
1416 |
+
|
1417 |
+
}, 500);
|
1418 |
+
}
|
1419 |
+
|
1420 |
+
// Initial animation
|
1421 |
+
setTimeout(() => {
|
1422 |
+
animateLettersIn(words[0]);
|
1423 |
+
}, 500);
|
1424 |
+
|
1425 |
+
// Start word transition cycle
|
1426 |
+
setInterval(transitionWords, 3500);
|
1427 |
+
|
1428 |
+
|
1429 |
+
const carousel = document.getElementById('carousel');
|
1430 |
+
const pages = document.querySelectorAll('.page');
|
1431 |
+
const nextBtn2 = document.getElementById('nextBtn2');
|
1432 |
+
const prevBtn = document.getElementById('prevBtn');
|
1433 |
+
const dots = document.querySelectorAll('.dot');
|
1434 |
+
const pageShadows = document.querySelectorAll('.page-shadow');
|
1435 |
+
|
1436 |
+
let currentPage = 0;
|
1437 |
+
let totalSlides = 6; // Confirm we have 6 total slides
|
1438 |
+
|
1439 |
+
// Initialize pages
|
1440 |
+
pages.forEach((page, index) => {
|
1441 |
+
page.style.transform = 'rotateY(0deg)';
|
1442 |
+
page.style.zIndex = 6 - index;
|
1443 |
+
});
|
1444 |
+
|
1445 |
+
// Function to turn to next page and rotate carousel
|
1446 |
+
function nextPage() {
|
1447 |
+
if (currentPage < pages.length) {
|
1448 |
+
// Apply shadow during turn
|
1449 |
+
if (currentPage > 0) {
|
1450 |
+
pageShadows[currentPage-1].style.opacity = '0';
|
1451 |
+
}
|
1452 |
+
|
1453 |
+
pages[currentPage].style.transform = 'rotateY(-180deg)';
|
1454 |
+
pages[currentPage].style.zIndex = 10 + currentPage;
|
1455 |
+
|
1456 |
+
if (currentPage < pages.length - 1) {
|
1457 |
+
pageShadows[currentPage].style.opacity = '1';
|
1458 |
+
}
|
1459 |
+
|
1460 |
+
currentPage++;
|
1461 |
+
|
1462 |
+
// Rotate carousel - Fixed rotation calculation for 6 slides (360 degrees)
|
1463 |
+
const newRotationDegree = (currentPage % totalSlides) * (360 / totalSlides);
|
1464 |
+
carousel.style.transform = `rotateY(${-newRotationDegree}deg)`;
|
1465 |
+
|
1466 |
+
// Update pagination dots
|
1467 |
+
updatePagination(currentPage % totalSlides);
|
1468 |
+
}
|
1469 |
+
}
|
1470 |
+
|
1471 |
+
// Function to turn to previous page and rotate carousel
|
1472 |
+
function prevPage() {
|
1473 |
+
if (currentPage > 0) {
|
1474 |
+
currentPage--;
|
1475 |
+
|
1476 |
+
// Apply shadow during turn
|
1477 |
+
if (currentPage > 0) {
|
1478 |
+
pageShadows[currentPage-1].style.opacity = '1';
|
1479 |
+
}
|
1480 |
+
|
1481 |
+
pages[currentPage].style.transform = 'rotateY(0deg)';
|
1482 |
+
setTimeout(() => {
|
1483 |
+
pages[currentPage].style.zIndex = 10 - currentPage;
|
1484 |
+
}, 300);
|
1485 |
+
|
1486 |
+
pageShadows[currentPage].style.opacity = '0';
|
1487 |
+
|
1488 |
+
// Rotate carousel - Fixed rotation calculation
|
1489 |
+
const newRotationDegree = (currentPage % totalSlides) * (360 / totalSlides);
|
1490 |
+
carousel.style.transform = `rotateY(${-newRotationDegree}deg)`;
|
1491 |
+
|
1492 |
+
// Update pagination dots
|
1493 |
+
updatePagination(currentPage % totalSlides);
|
1494 |
+
}
|
1495 |
+
}
|
1496 |
+
|
1497 |
+
// Function to reset book to beginning
|
1498 |
+
function resetBook() {
|
1499 |
+
pages.forEach((page, index) => {
|
1500 |
+
page.style.transition = 'none';
|
1501 |
+
page.style.transform = 'rotateY(0deg)';
|
1502 |
+
setTimeout(() => {
|
1503 |
+
page.style.zIndex = 5 - index;
|
1504 |
+
page.style.transition = 'transform 0.6s ease, z-index 0s 0.3s';
|
1505 |
+
}, 50);
|
1506 |
+
});
|
1507 |
+
|
1508 |
+
pageShadows.forEach(shadow => {
|
1509 |
+
shadow.style.opacity = '0';
|
1510 |
+
});
|
1511 |
+
|
1512 |
+
currentPage = 0;
|
1513 |
+
|
1514 |
+
// Reset carousel and pagination
|
1515 |
+
carousel.style.transform = 'rotateY(0deg)';
|
1516 |
+
updatePagination(0);
|
1517 |
+
}
|
1518 |
+
|
1519 |
+
// Function to update pagination dots
|
1520 |
+
function updatePagination(index) {
|
1521 |
+
dots.forEach((dot, i) => {
|
1522 |
+
if (i === index) {
|
1523 |
+
dot.classList.add('active');
|
1524 |
+
} else {
|
1525 |
+
dot.classList.remove('active');
|
1526 |
+
}
|
1527 |
+
});
|
1528 |
+
}
|
1529 |
+
|
1530 |
+
// Event listeners
|
1531 |
+
nextBtn2.addEventListener('click', nextPage);
|
1532 |
+
prevBtn.addEventListener('click', prevPage);
|
1533 |
+
|
1534 |
+
// Make pagination dots clickable
|
1535 |
+
dots.forEach((dot, index) => {
|
1536 |
+
dot.addEventListener('click', () => {
|
1537 |
+
// Find the current actual position in the book
|
1538 |
+
const currentInCycle = currentPage % totalSlides;
|
1539 |
+
|
1540 |
+
// Calculate how many pages to move
|
1541 |
+
let pagesToMove;
|
1542 |
+
|
1543 |
+
if (index > currentInCycle) {
|
1544 |
+
pagesToMove = index - currentInCycle;
|
1545 |
+
} else if (index < currentInCycle) {
|
1546 |
+
pagesToMove = totalSlides - currentInCycle + index;
|
1547 |
+
} else {
|
1548 |
+
return; // Already on this page
|
1549 |
+
}
|
1550 |
+
|
1551 |
+
// Move the pages
|
1552 |
+
for (let i = 0; i < pagesToMove; i++) {
|
1553 |
+
setTimeout(() => nextPage(), i * 400);
|
1554 |
+
}
|
1555 |
+
});
|
1556 |
+
});
|
1557 |
+
|
1558 |
+
// Allow clicking on pages to turn them
|
1559 |
+
pages.forEach((page, index) => {
|
1560 |
+
page.addEventListener('click', () => {
|
1561 |
+
if (index === currentPage - 1) {
|
1562 |
+
prevPage();
|
1563 |
+
} else if (index === currentPage) {
|
1564 |
+
nextPage();
|
1565 |
+
}
|
1566 |
+
});
|
1567 |
+
});
|
1568 |
+
|
1569 |
+
const canvas = document.getElementById('bgParticles');
|
1570 |
+
if (canvas) {
|
1571 |
+
const ctx = canvas.getContext('2d');
|
1572 |
+
let particles = [];
|
1573 |
+
const colors = ['#ff6ec4', '#7873f5', '#1fd1f9', '#43e97b'];
|
1574 |
+
let width = window.innerWidth;
|
1575 |
+
let height = window.innerHeight;
|
1576 |
+
canvas.width = width;
|
1577 |
+
canvas.height = height;
|
1578 |
+
|
1579 |
+
function randomBetween(a, b) { return a + Math.random() * (b - a); }
|
1580 |
+
|
1581 |
+
function createParticles(num) {
|
1582 |
+
particles = [];
|
1583 |
+
for (let i = 0; i < num; i++) {
|
1584 |
+
particles.push({
|
1585 |
+
x: randomBetween(0, width),
|
1586 |
+
y: randomBetween(0, height),
|
1587 |
+
r: randomBetween(2, 5),
|
1588 |
+
color: colors[Math.floor(Math.random() * colors.length)],
|
1589 |
+
dx: randomBetween(-0.7, 0.7),
|
1590 |
+
dy: randomBetween(-0.7, 0.7),
|
1591 |
+
alpha: randomBetween(0.3, 0.8)
|
1592 |
+
});
|
1593 |
+
}
|
1594 |
+
}
|
1595 |
+
|
1596 |
+
function drawParticles() {
|
1597 |
+
ctx.clearRect(0, 0, width, height);
|
1598 |
+
for (let p of particles) {
|
1599 |
+
ctx.save();
|
1600 |
+
ctx.globalAlpha = p.alpha;
|
1601 |
+
ctx.beginPath();
|
1602 |
+
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
1603 |
+
ctx.fillStyle = p.color;
|
1604 |
+
ctx.shadowColor = p.color;
|
1605 |
+
ctx.shadowBlur = 16;
|
1606 |
+
ctx.fill();
|
1607 |
+
ctx.restore();
|
1608 |
+
|
1609 |
+
p.x += p.dx;
|
1610 |
+
p.y += p.dy;
|
1611 |
+
if (p.x < 0 || p.x > width) p.dx *= -1;
|
1612 |
+
if (p.y < 0 || p.y > height) p.dy *= -1;
|
1613 |
+
}
|
1614 |
+
requestAnimationFrame(drawParticles);
|
1615 |
+
}
|
1616 |
+
|
1617 |
+
createParticles(48);
|
1618 |
+
drawParticles();
|
1619 |
+
|
1620 |
+
window.addEventListener('resize', () => {
|
1621 |
+
width = window.innerWidth;
|
1622 |
+
height = window.innerHeight;
|
1623 |
+
canvas.width = width;
|
1624 |
+
canvas.height = height;
|
1625 |
+
createParticles(48);
|
1626 |
+
});
|
1627 |
+
}
|
static/js/login.js
ADDED
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Firebase configuration
|
2 |
+
const firebaseConfig = {
|
3 |
+
apiKey: "AIzaSyA__OFybLoWSA6x_8lIyo0Z1Cb2ozhbHoA",
|
4 |
+
authDomain: "capstone-project-5f445.firebaseapp.com",
|
5 |
+
projectId: "capstone-project-5f445",
|
6 |
+
storageBucket: "capstone-project-5f445.firebasestorage.app",
|
7 |
+
messagingSenderId: "701976065811",
|
8 |
+
appId: "1:701976065811:web:494fa4ea19f04f1dd00ce6",
|
9 |
+
measurementId: "G-EVZTJ9X538"
|
10 |
+
};
|
11 |
+
|
12 |
+
// Initialize Firebase
|
13 |
+
const app = firebase.initializeApp(firebaseConfig);
|
14 |
+
const auth = firebase.auth();
|
15 |
+
const db = firebase.firestore();
|
16 |
+
|
17 |
+
// Form Elements
|
18 |
+
const signUpForm = document.querySelector('.sign-up');
|
19 |
+
const signInForm = document.querySelector('.sign-in');
|
20 |
+
const forgotForm = document.querySelector('.forgot-password');
|
21 |
+
|
22 |
+
// Sign Up Handler
|
23 |
+
signUpForm.addEventListener('submit', async (event) => {
|
24 |
+
event.preventDefault();
|
25 |
+
|
26 |
+
const name = document.querySelector('.sign-up input[type="text"]').value;
|
27 |
+
const email = document.querySelector('.sign-up input[type="email"]').value;
|
28 |
+
const password = document.querySelectorAll('.sign-up input[type="password"]')[0].value;
|
29 |
+
const confirmPassword = document.querySelectorAll('.sign-up input[type="password"]')[1].value;
|
30 |
+
|
31 |
+
if (password !== confirmPassword) {
|
32 |
+
alert("Passwords don't match!");
|
33 |
+
return;
|
34 |
+
}
|
35 |
+
|
36 |
+
try {
|
37 |
+
const userCredential = await auth.createUserWithEmailAndPassword(email, password);
|
38 |
+
const user = userCredential.user;
|
39 |
+
|
40 |
+
// Save to Firestore
|
41 |
+
await db.collection('users').doc(user.uid).set({
|
42 |
+
name: name,
|
43 |
+
email: email,
|
44 |
+
createdAt: firebase.firestore.FieldValue.serverTimestamp()
|
45 |
+
});
|
46 |
+
|
47 |
+
// Show success message and switch forms
|
48 |
+
alert('Account created successfully! Redirecting to login...');
|
49 |
+
|
50 |
+
// Trigger sign-in form animation
|
51 |
+
wrapper.classList.add('animated-signup');
|
52 |
+
wrapper.classList.remove('animated-signin', 'animated-forgot');
|
53 |
+
|
54 |
+
} catch (error) {
|
55 |
+
alert(error.message);
|
56 |
+
console.error("Signup error:", error);
|
57 |
+
}
|
58 |
+
});
|
59 |
+
|
60 |
+
// Animation utility functions
|
61 |
+
function scatterPapers() {
|
62 |
+
// Create paper particles
|
63 |
+
for (let i = 0; i < 10; i++) {
|
64 |
+
const page = document.createElement('div');
|
65 |
+
page.classList.add('page');
|
66 |
+
page.style.left = '50%';
|
67 |
+
page.style.top = '50%';
|
68 |
+
page.style.transform = 'translate(-50%, -50%)';
|
69 |
+
page.style.setProperty('--x', `${Math.random() * 800 - 400}px`);
|
70 |
+
page.style.setProperty('--y', `${Math.random() * -600}px`);
|
71 |
+
page.style.setProperty('--rotate', `${Math.random() * 720 - 360}deg`);
|
72 |
+
document.body.appendChild(page);
|
73 |
+
|
74 |
+
setTimeout(() => page.remove(), 3000);
|
75 |
+
}
|
76 |
+
}
|
77 |
+
|
78 |
+
function tearPaper() {
|
79 |
+
const leftPiece = document.createElement('div');
|
80 |
+
leftPiece.classList.add('tear-left');
|
81 |
+
leftPiece.style.left = 'calc(50% - 75px)';
|
82 |
+
leftPiece.style.top = '50%';
|
83 |
+
leftPiece.style.transform = 'translateY(-50%)';
|
84 |
+
document.body.appendChild(leftPiece);
|
85 |
+
|
86 |
+
const rightPiece = document.createElement('div');
|
87 |
+
rightPiece.classList.add('tear-right');
|
88 |
+
rightPiece.style.left = '50%';
|
89 |
+
rightPiece.style.top = '50%';
|
90 |
+
rightPiece.style.transform = 'translateY(-50%)';
|
91 |
+
document.body.appendChild(rightPiece);
|
92 |
+
|
93 |
+
setTimeout(() => {
|
94 |
+
leftPiece.remove();
|
95 |
+
rightPiece.remove();
|
96 |
+
}, 7000);
|
97 |
+
}
|
98 |
+
|
99 |
+
signInForm.addEventListener('submit', async (event) => {
|
100 |
+
event.preventDefault();
|
101 |
+
|
102 |
+
const email = document.querySelector('.sign-in input[type="email"]').value;
|
103 |
+
const password = document.querySelector('.sign-in input[type="password"]').value;
|
104 |
+
|
105 |
+
try {
|
106 |
+
const userCredential = await auth.signInWithEmailAndPassword(email, password);
|
107 |
+
const user = userCredential.user;
|
108 |
+
|
109 |
+
// Fetch username from Firestore
|
110 |
+
const userDoc = await db.collection('users').doc(user.uid).get();
|
111 |
+
let username = "User";
|
112 |
+
if (userDoc.exists) {
|
113 |
+
username = userDoc.data().name;
|
114 |
+
}
|
115 |
+
|
116 |
+
// Store username and userId in localStorage
|
117 |
+
localStorage.setItem("username", username);
|
118 |
+
localStorage.setItem("userId", user.uid);
|
119 |
+
|
120 |
+
// Trigger success animation
|
121 |
+
scatterPapers();
|
122 |
+
|
123 |
+
// Redirect to home page after animation
|
124 |
+
setTimeout(() => {
|
125 |
+
window.location.href = "/home";
|
126 |
+
}, 2000);
|
127 |
+
|
128 |
+
} catch (error) {
|
129 |
+
// Trigger failure animation
|
130 |
+
tearPaper();
|
131 |
+
console.error("Login error:", error);
|
132 |
+
|
133 |
+
}
|
134 |
+
});
|
135 |
+
|
136 |
+
// Password Reset Handler
|
137 |
+
forgotForm.addEventListener('submit', async (event) => {
|
138 |
+
event.preventDefault();
|
139 |
+
const email = document.querySelector('.forgot-password input[type="email"]').value;
|
140 |
+
|
141 |
+
try {
|
142 |
+
await auth.sendPasswordResetEmail(email);
|
143 |
+
alert('Password reset email sent! Check your inbox.');
|
144 |
+
} catch (error) {
|
145 |
+
alert(error.message);
|
146 |
+
console.error("Password reset error:", error);
|
147 |
+
}
|
148 |
+
});
|
149 |
+
|
150 |
+
// Form navigation handlers
|
151 |
+
let wrapper = document.querySelector('.wrapper');
|
152 |
+
const signUpLink = document.querySelector('.signup-link');
|
153 |
+
const signInLink = document.querySelector('.signin-link');
|
154 |
+
const forgotPassLink = document.querySelector('.forgot-pass a');
|
155 |
+
const backToLoginLink = document.querySelector('.forget-link');
|
156 |
+
|
157 |
+
// Set initial animation state
|
158 |
+
wrapper.classList.add('animated-signup');
|
159 |
+
|
160 |
+
// Form switching event listeners
|
161 |
+
signUpLink.addEventListener('click', () => {
|
162 |
+
wrapper.classList.add('animated-signin');
|
163 |
+
wrapper.classList.remove('animated-signup', 'animated-forgot');
|
164 |
+
});
|
165 |
+
|
166 |
+
signInLink.addEventListener('click', () => {
|
167 |
+
wrapper.classList.add('animated-signup');
|
168 |
+
wrapper.classList.remove('animated-signin', 'animated-forgot');
|
169 |
+
});
|
170 |
+
|
171 |
+
forgotPassLink.addEventListener('click', () => {
|
172 |
+
wrapper.classList.add('animated-forgot');
|
173 |
+
wrapper.classList.remove('animated-signin', 'animated-signup');
|
174 |
+
});
|
175 |
+
|
176 |
+
backToLoginLink.addEventListener('click', () => {
|
177 |
+
wrapper.classList.add('animated-signup');
|
178 |
+
wrapper.classList.remove('animated-forgot');
|
179 |
+
});
|
180 |
+
|
181 |
+
// Paper falling animation
|
182 |
+
document.addEventListener("DOMContentLoaded", () => {
|
183 |
+
const paperFallContainer = document.querySelector('.paper-fall');
|
184 |
+
function createPaper() {
|
185 |
+
const paper = document.createElement('div');
|
186 |
+
paper.classList.add('paper');
|
187 |
+
|
188 |
+
// Randomize the size of the paper
|
189 |
+
const size = Math.random() * 30 + 20; // Between 20px and 50px
|
190 |
+
paper.style.width = `${size}px`;
|
191 |
+
paper.style.height = `${size}px`;
|
192 |
+
|
193 |
+
// Randomize the position horizontally
|
194 |
+
const randomX = Math.random() * window.innerWidth;
|
195 |
+
paper.style.left = `${randomX}px`;
|
196 |
+
|
197 |
+
// Randomize the animation duration
|
198 |
+
const duration = Math.random() * 5 + 5; // Between 5s and 10s
|
199 |
+
paper.style.animationDuration = `${duration}s`;
|
200 |
+
|
201 |
+
// Randomize the rotation
|
202 |
+
const rotation = Math.random() * 360;
|
203 |
+
paper.style.transform = `rotate(${rotation}deg)`;
|
204 |
+
|
205 |
+
// Append the paper to the container
|
206 |
+
paperFallContainer.appendChild(paper);
|
207 |
+
|
208 |
+
// Remove the paper after it falls out of view
|
209 |
+
setTimeout(() => {
|
210 |
+
paper.remove();
|
211 |
+
}, duration * 1000);
|
212 |
+
}
|
213 |
+
|
214 |
+
// Create a new paper every 100ms
|
215 |
+
setInterval(createPaper, 100);
|
216 |
+
});
|
static/js/useractivity.js
ADDED
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
document.addEventListener("DOMContentLoaded", async () => {
|
2 |
+
// Firebase Auth & Firestore setup - reference from existing code
|
3 |
+
const auth = firebase.auth();
|
4 |
+
const db = firebase.firestore();
|
5 |
+
|
6 |
+
// Activity tab elements
|
7 |
+
const activityTab = document.getElementById("activity-tab");
|
8 |
+
const searchCountElement = activityTab.querySelector(".stat-value");
|
9 |
+
const timelineContainer = activityTab.querySelector(".timeline");
|
10 |
+
|
11 |
+
// Function to format relative time (e.g., "2 days ago")
|
12 |
+
function getRelativeTimeString(timestamp) {
|
13 |
+
const now = new Date();
|
14 |
+
const searchTime = timestamp.toDate();
|
15 |
+
const diffInMs = now - searchTime;
|
16 |
+
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
17 |
+
|
18 |
+
if (diffInDays === 0) {
|
19 |
+
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
|
20 |
+
if (diffInHours === 0) {
|
21 |
+
const diffInMinutes = Math.floor(diffInMs / (1000 * 60));
|
22 |
+
return diffInMinutes === 1 ? "1 minute ago" : `${diffInMinutes} minutes ago`;
|
23 |
+
}
|
24 |
+
return diffInHours === 1 ? "1 hour ago" : `${diffInHours} hours ago`;
|
25 |
+
} else if (diffInDays === 1) {
|
26 |
+
return "Yesterday";
|
27 |
+
} else if (diffInDays < 7) {
|
28 |
+
return `${diffInDays} days ago`;
|
29 |
+
} else if (diffInDays < 30) {
|
30 |
+
const diffInWeeks = Math.floor(diffInDays / 7);
|
31 |
+
return diffInWeeks === 1 ? "1 week ago" : `${diffInWeeks} weeks ago`;
|
32 |
+
} else {
|
33 |
+
return searchTime.toLocaleDateString();
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
// Function to create a timeline item
|
38 |
+
function createTimelineItem(activity) {
|
39 |
+
const timelineItem = document.createElement("div");
|
40 |
+
timelineItem.className = "timeline-item";
|
41 |
+
|
42 |
+
const timelineDot = document.createElement("div");
|
43 |
+
timelineDot.className = "timeline-dot";
|
44 |
+
|
45 |
+
const timelineContent = document.createElement("div");
|
46 |
+
timelineContent.className = "timeline-content";
|
47 |
+
|
48 |
+
const title = document.createElement("h4");
|
49 |
+
title.textContent = activity.type === "search" ? activity.query : activity.title;
|
50 |
+
|
51 |
+
const description = document.createElement("p");
|
52 |
+
if (activity.type === "search") {
|
53 |
+
description.textContent = `You searched for "${activity.query}"`;
|
54 |
+
} else if (activity.type === "save") {
|
55 |
+
description.textContent = `You saved "${activity.title}"`;
|
56 |
+
} else if (activity.type === "profile") {
|
57 |
+
description.textContent = "You updated your profile information";
|
58 |
+
}
|
59 |
+
|
60 |
+
const timelineDate = document.createElement("span");
|
61 |
+
timelineDate.className = "timeline-date";
|
62 |
+
|
63 |
+
const clockIcon = document.createElement("i");
|
64 |
+
clockIcon.className = "fas fa-clock";
|
65 |
+
|
66 |
+
timelineDate.appendChild(clockIcon);
|
67 |
+
timelineDate.append(` ${getRelativeTimeString(activity.timestamp)}`);
|
68 |
+
|
69 |
+
timelineContent.appendChild(title);
|
70 |
+
timelineContent.appendChild(description);
|
71 |
+
timelineContent.appendChild(timelineDate);
|
72 |
+
|
73 |
+
timelineItem.appendChild(timelineDot);
|
74 |
+
timelineItem.appendChild(timelineContent);
|
75 |
+
|
76 |
+
return timelineItem;
|
77 |
+
}
|
78 |
+
|
79 |
+
// Function to load and display user activity
|
80 |
+
async function loadUserActivity() {
|
81 |
+
const user = auth.currentUser;
|
82 |
+
if (!user) return;
|
83 |
+
|
84 |
+
try {
|
85 |
+
// Get user activity from Firestore
|
86 |
+
const activitiesSnapshot = await db.collection("users")
|
87 |
+
.doc(user.uid)
|
88 |
+
.collection("activities")
|
89 |
+
.orderBy("timestamp", "desc")
|
90 |
+
.limit(10)
|
91 |
+
.get();
|
92 |
+
|
93 |
+
// Clear existing timeline content
|
94 |
+
timelineContainer.innerHTML = "";
|
95 |
+
|
96 |
+
// Track search count
|
97 |
+
let searchCount = 0;
|
98 |
+
|
99 |
+
if (!activitiesSnapshot.empty) {
|
100 |
+
activitiesSnapshot.forEach(doc => {
|
101 |
+
const activity = doc.data();
|
102 |
+
|
103 |
+
// Count searches for the stat display
|
104 |
+
if (activity.type === "search") {
|
105 |
+
searchCount++;
|
106 |
+
}
|
107 |
+
|
108 |
+
// Create and append timeline item
|
109 |
+
const timelineItem = createTimelineItem(activity);
|
110 |
+
timelineContainer.appendChild(timelineItem);
|
111 |
+
});
|
112 |
+
|
113 |
+
// Update search count display
|
114 |
+
searchCountElement.textContent = searchCount;
|
115 |
+
} else {
|
116 |
+
// No activities found
|
117 |
+
searchCountElement.textContent = "0";
|
118 |
+
|
119 |
+
// Display a message when no activities are found
|
120 |
+
const noActivitiesMessage = document.createElement("div");
|
121 |
+
noActivitiesMessage.className = "empty-state";
|
122 |
+
noActivitiesMessage.innerHTML = `
|
123 |
+
<i class="fas fa-history"></i>
|
124 |
+
<p>No activity history found. Your recent searches and actions will appear here.</p>
|
125 |
+
`;
|
126 |
+
timelineContainer.appendChild(noActivitiesMessage);
|
127 |
+
}
|
128 |
+
} catch (error) {
|
129 |
+
console.error("Error loading user activity:", error);
|
130 |
+
timelineContainer.innerHTML = `<div class="error-message">Failed to load activity history.</div>`;
|
131 |
+
}
|
132 |
+
}
|
133 |
+
|
134 |
+
// Function to save search activity
|
135 |
+
window.saveSearchActivity = async (query) => {
|
136 |
+
const user = auth.currentUser;
|
137 |
+
if (!user || !query) return;
|
138 |
+
|
139 |
+
try {
|
140 |
+
// Create activity object
|
141 |
+
const activity = {
|
142 |
+
type: "search",
|
143 |
+
query: query,
|
144 |
+
timestamp: firebase.firestore.FieldValue.serverTimestamp()
|
145 |
+
};
|
146 |
+
|
147 |
+
// Save to Firestore
|
148 |
+
await db.collection("users")
|
149 |
+
.doc(user.uid)
|
150 |
+
.collection("activities")
|
151 |
+
.add(activity);
|
152 |
+
|
153 |
+
console.log("Search activity saved successfully");
|
154 |
+
} catch (error) {
|
155 |
+
console.error("Error saving search activity:", error);
|
156 |
+
}
|
157 |
+
};
|
158 |
+
|
159 |
+
// Function to save profile update activity
|
160 |
+
window.saveProfileUpdateActivity = async () => {
|
161 |
+
const user = auth.currentUser;
|
162 |
+
if (!user) return;
|
163 |
+
|
164 |
+
try {
|
165 |
+
// Create activity object
|
166 |
+
const activity = {
|
167 |
+
type: "profile",
|
168 |
+
title: "Profile Updated",
|
169 |
+
timestamp: firebase.firestore.FieldValue.serverTimestamp()
|
170 |
+
};
|
171 |
+
|
172 |
+
// Save to Firestore
|
173 |
+
await db.collection("users")
|
174 |
+
.doc(user.uid)
|
175 |
+
.collection("activities")
|
176 |
+
.add(activity);
|
177 |
+
|
178 |
+
console.log("Profile update activity saved successfully");
|
179 |
+
} catch (error) {
|
180 |
+
console.error("Error saving profile update activity:", error);
|
181 |
+
}
|
182 |
+
};
|
183 |
+
|
184 |
+
// Function to save article activity
|
185 |
+
window.saveArticleActivity = async (title, articleId) => {
|
186 |
+
const user = auth.currentUser;
|
187 |
+
if (!user || !title) return;
|
188 |
+
|
189 |
+
try {
|
190 |
+
// Create activity object
|
191 |
+
const activity = {
|
192 |
+
type: "save",
|
193 |
+
title: title,
|
194 |
+
articleId: articleId || null,
|
195 |
+
timestamp: firebase.firestore.FieldValue.serverTimestamp()
|
196 |
+
};
|
197 |
+
|
198 |
+
// Save to Firestore
|
199 |
+
await db.collection("users")
|
200 |
+
.doc(user.uid)
|
201 |
+
.collection("activities")
|
202 |
+
.add(activity);
|
203 |
+
|
204 |
+
console.log("Article save activity recorded successfully");
|
205 |
+
} catch (error) {
|
206 |
+
console.error("Error saving article activity:", error);
|
207 |
+
}
|
208 |
+
};
|
209 |
+
|
210 |
+
// Load activity when tab is clicked
|
211 |
+
const activityTabBtn = document.querySelector('[data-tab="activity"]');
|
212 |
+
if (activityTabBtn) {
|
213 |
+
activityTabBtn.addEventListener("click", loadUserActivity);
|
214 |
+
}
|
215 |
+
|
216 |
+
// Update the save profile button to also record the activity
|
217 |
+
const saveBtn = document.getElementById("saveBtn");
|
218 |
+
const originalClickHandler = saveBtn.onclick;
|
219 |
+
|
220 |
+
saveBtn.addEventListener("click", async () => {
|
221 |
+
// Call the original handler first
|
222 |
+
if (typeof originalClickHandler === "function") {
|
223 |
+
originalClickHandler();
|
224 |
+
}
|
225 |
+
|
226 |
+
// Add activity tracking
|
227 |
+
await saveProfileUpdateActivity();
|
228 |
+
});
|
229 |
+
|
230 |
+
// Hook this up to your search functionality
|
231 |
+
// Example: If you have a search form
|
232 |
+
const searchForm = document.getElementById("searchForm");
|
233 |
+
if (searchForm) {
|
234 |
+
searchForm.addEventListener("submit", (e) => {
|
235 |
+
const searchInput = searchForm.querySelector("input[type=search]");
|
236 |
+
if (searchInput && searchInput.value.trim()) {
|
237 |
+
saveSearchActivity(searchInput.value.trim());
|
238 |
+
}
|
239 |
+
});
|
240 |
+
}
|
241 |
+
|
242 |
+
// Load activity data when the page loads if user is on activity tab
|
243 |
+
auth.onAuthStateChanged(user => {
|
244 |
+
if (user && activityTab.classList.contains("active")) {
|
245 |
+
loadUserActivity();
|
246 |
+
}
|
247 |
+
});
|
248 |
+
});
|
249 |
+
|
250 |
+
|
251 |
+
|
templates/contactBoard.html
ADDED
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Team Member Contact </title>
|
7 |
+
<style>
|
8 |
+
@import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
|
9 |
+
:root {
|
10 |
+
--primary-color: #4a90e2;
|
11 |
+
--secondary-color: #fcee0a;
|
12 |
+
--third-color: #6f42c1;
|
13 |
+
--text-color: #333;
|
14 |
+
--bg-color: #f5f7fa;
|
15 |
+
--white: #ffffff;
|
16 |
+
--forth-color: #00BFFF;
|
17 |
+
--aqua:#28cbb0;
|
18 |
+
}
|
19 |
+
|
20 |
+
* {
|
21 |
+
margin: 0;
|
22 |
+
padding: 0;
|
23 |
+
box-sizing: border-box;
|
24 |
+
font-family: 'Poppins', sans-serif;
|
25 |
+
}
|
26 |
+
|
27 |
+
body {
|
28 |
+
background-color: var(--secondary-color);
|
29 |
+
padding: 40px 20px;
|
30 |
+
}
|
31 |
+
|
32 |
+
.container {
|
33 |
+
max-width: 1200px;
|
34 |
+
margin: 0 auto;
|
35 |
+
padding: 20px;
|
36 |
+
}
|
37 |
+
|
38 |
+
.header {
|
39 |
+
text-align: center;
|
40 |
+
margin-bottom: 50px;
|
41 |
+
}
|
42 |
+
|
43 |
+
.header h1 {
|
44 |
+
font-size: 2.5rem;
|
45 |
+
color: var(--text-color);
|
46 |
+
margin-bottom: 15px;
|
47 |
+
position: relative;
|
48 |
+
display: inline-block;
|
49 |
+
font-family: 'Bungee Shade';
|
50 |
+
}
|
51 |
+
|
52 |
+
.header h1::after {
|
53 |
+
content: '';
|
54 |
+
position: absolute;
|
55 |
+
bottom: -10px;
|
56 |
+
left: 50%;
|
57 |
+
transform: translateX(-50%);
|
58 |
+
width: 80px;
|
59 |
+
height: 4px;
|
60 |
+
background: var(--forth-color);
|
61 |
+
border-radius: 2px;
|
62 |
+
}
|
63 |
+
|
64 |
+
.header p {
|
65 |
+
|
66 |
+
font-size: 1.1rem;
|
67 |
+
max-width: 700px;
|
68 |
+
margin: 0 auto;
|
69 |
+
font-family: 'Bungee Shade';
|
70 |
+
color: black;
|
71 |
+
}
|
72 |
+
|
73 |
+
.cards-container {
|
74 |
+
display: flex;
|
75 |
+
flex-wrap: wrap;
|
76 |
+
justify-content: center;
|
77 |
+
gap: 30px;
|
78 |
+
}
|
79 |
+
|
80 |
+
.card {
|
81 |
+
flex: 1;
|
82 |
+
min-width: 300px;
|
83 |
+
max-width: 350px;
|
84 |
+
background: var(--text-color);
|
85 |
+
border-radius: 15px;
|
86 |
+
overflow: hidden;
|
87 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
88 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
89 |
+
position: relative;
|
90 |
+
padding-top: 40px;
|
91 |
+
}
|
92 |
+
|
93 |
+
.card:hover {
|
94 |
+
transform: translateY(-10px);
|
95 |
+
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
|
96 |
+
}
|
97 |
+
|
98 |
+
.card-number {
|
99 |
+
position: absolute;
|
100 |
+
top: 20px;
|
101 |
+
left: 20px;
|
102 |
+
width: 40px;
|
103 |
+
height: 40px;
|
104 |
+
background: var(--secondary-color);
|
105 |
+
color: var(--text-color);
|
106 |
+
border-radius: 50%;
|
107 |
+
display: flex;
|
108 |
+
justify-content: center;
|
109 |
+
align-items: center;
|
110 |
+
font-weight: bold;
|
111 |
+
font-size: 1.2rem;
|
112 |
+
box-shadow: 0 5px 15px rgba(252, 238, 10, 0.3);
|
113 |
+
z-index: 2;
|
114 |
+
}
|
115 |
+
|
116 |
+
.card-title {
|
117 |
+
text-align: center;
|
118 |
+
padding: 0 20px;
|
119 |
+
margin-bottom: 20px;
|
120 |
+
}
|
121 |
+
|
122 |
+
.card-title h3 {
|
123 |
+
font-size: 1.5rem;
|
124 |
+
color: var(--text-color);
|
125 |
+
margin-bottom: 5px;
|
126 |
+
}
|
127 |
+
|
128 |
+
.card-title p {
|
129 |
+
font-size: 0.9rem;
|
130 |
+
color: #666;
|
131 |
+
}
|
132 |
+
|
133 |
+
.member-photo {
|
134 |
+
width: 120px;
|
135 |
+
height: 120px;
|
136 |
+
border-radius: 50%;
|
137 |
+
border: 5px solid white;
|
138 |
+
object-fit: cover;
|
139 |
+
margin: 0 auto 20px;
|
140 |
+
display: block;
|
141 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
142 |
+
background-color: #f0f0f0;
|
143 |
+
}
|
144 |
+
|
145 |
+
.card-body {
|
146 |
+
padding: 0 30px 30px;
|
147 |
+
}
|
148 |
+
|
149 |
+
.contact-info {
|
150 |
+
margin-bottom: 25px;
|
151 |
+
}
|
152 |
+
|
153 |
+
.contact-item {
|
154 |
+
display: flex;
|
155 |
+
align-items: center;
|
156 |
+
margin-bottom: 15px;
|
157 |
+
}
|
158 |
+
|
159 |
+
.contact-icon {
|
160 |
+
width: 35px;
|
161 |
+
height: 35px;
|
162 |
+
background: var(--forth-color);
|
163 |
+
border-radius: 50%;
|
164 |
+
display: flex;
|
165 |
+
justify-content: center;
|
166 |
+
align-items: center;
|
167 |
+
margin-right: 15px;
|
168 |
+
color: white;
|
169 |
+
font-size: 16px;
|
170 |
+
flex-shrink: 0;
|
171 |
+
}
|
172 |
+
|
173 |
+
.contact-text {
|
174 |
+
font-size: 0.9rem;
|
175 |
+
color: var(--secondary-color);
|
176 |
+
font-weight: bolder;
|
177 |
+
}
|
178 |
+
|
179 |
+
|
180 |
+
.contact-text strong {
|
181 |
+
color: var(--text-color);
|
182 |
+
display: block;
|
183 |
+
margin-bottom: 3px;
|
184 |
+
}
|
185 |
+
|
186 |
+
.social-links {
|
187 |
+
display: flex;
|
188 |
+
justify-content: center;
|
189 |
+
gap: 15px;
|
190 |
+
margin-top: 30px;
|
191 |
+
}
|
192 |
+
|
193 |
+
.social-link {
|
194 |
+
display: flex;
|
195 |
+
justify-content: center;
|
196 |
+
align-items: center;
|
197 |
+
width: 40px;
|
198 |
+
height: 40px;
|
199 |
+
border-radius: 50%;
|
200 |
+
background: var(--white);
|
201 |
+
color: var(--forth-color);
|
202 |
+
font-size: 18px;
|
203 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
204 |
+
transition: all 0.3s ease;
|
205 |
+
}
|
206 |
+
|
207 |
+
.social-link:hover {
|
208 |
+
transform: translateY(-5px) rotate(15deg);
|
209 |
+
background: var(--forth-color);
|
210 |
+
color: var(--white);
|
211 |
+
box-shadow: 0 8px 20px rgba(0, 191, 255, 0.3);
|
212 |
+
}
|
213 |
+
|
214 |
+
@media (max-width: 768px) {
|
215 |
+
.cards-container {
|
216 |
+
flex-direction: column;
|
217 |
+
align-items: center;
|
218 |
+
}
|
219 |
+
|
220 |
+
.card {
|
221 |
+
max-width: 100%;
|
222 |
+
}
|
223 |
+
}
|
224 |
+
|
225 |
+
</style>
|
226 |
+
</head>
|
227 |
+
<body>
|
228 |
+
<div class="container">
|
229 |
+
<div class="header">
|
230 |
+
<h1>Our Team Members</h1>
|
231 |
+
<p>Get in touch with our dedicated team who will provide you with quality service</p>
|
232 |
+
</div>
|
233 |
+
|
234 |
+
<div class="cards-container">
|
235 |
+
<!-- Card 1 -->
|
236 |
+
<div class="card">
|
237 |
+
<div class="card-number">01</div>
|
238 |
+
<img src="/assets/dev1.png" alt="Member Photo" class="member-photo">
|
239 |
+
<div class="card-body">
|
240 |
+
<div class="contact-info">
|
241 |
+
<div class="contact-item">
|
242 |
+
<div class="contact-icon">
|
243 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
244 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
245 |
+
<circle cx="12" cy="7" r="4"></circle>
|
246 |
+
</svg>
|
247 |
+
</div>
|
248 |
+
<div class="contact-text">
|
249 |
+
<strong style="color: #fcee0a;">Name: </strong>
|
250 |
+
<sub style="margin-left: 10px;">Javvadi Jaswant Sri Akhilesh</sub>
|
251 |
+
|
252 |
+
</div>
|
253 |
+
</div>
|
254 |
+
<div class="contact-item">
|
255 |
+
<div class="contact-icon">
|
256 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
257 |
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
258 |
+
</svg>
|
259 |
+
</div>
|
260 |
+
<div class="contact-text">
|
261 |
+
<strong style="color: #fcee0a;">Phone</strong>
|
262 |
+
<sub style="margin-left: 10px;">+91-9632587415</sub>
|
263 |
+
</div>
|
264 |
+
</div>
|
265 |
+
<div class="contact-item">
|
266 |
+
<div class="contact-icon">
|
267 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
268 |
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
269 |
+
<polyline points="22,6 12,13 2,6"></polyline>
|
270 |
+
</svg>
|
271 |
+
</div>
|
272 |
+
<div class="contact-text">
|
273 |
+
<strong style="color: #fcee0a;">Email</strong>
|
274 |
+
<sub style="margin-left: 10px;">[email protected]</sub>
|
275 |
+
</div>
|
276 |
+
</div>
|
277 |
+
</div>
|
278 |
+
<div class="social-links">
|
279 |
+
<a href="#" class="social-link">
|
280 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
281 |
+
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
|
282 |
+
</svg>
|
283 |
+
</a>
|
284 |
+
<a href="#" class="social-link">
|
285 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
286 |
+
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
287 |
+
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
288 |
+
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
289 |
+
</svg>
|
290 |
+
</a>
|
291 |
+
<a href="#" class="social-link">
|
292 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
293 |
+
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
|
294 |
+
</svg>
|
295 |
+
</a>
|
296 |
+
<a href="#" class="social-link">
|
297 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
298 |
+
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
|
299 |
+
<rect x="2" y="9" width="4" height="12"></rect>
|
300 |
+
<circle cx="4" cy="4" r="2"></circle>
|
301 |
+
</svg>
|
302 |
+
</a>
|
303 |
+
</div>
|
304 |
+
</div>
|
305 |
+
</div>
|
306 |
+
|
307 |
+
<!-- Card 2 -->
|
308 |
+
<div class="card">
|
309 |
+
<div class="card-number">02</div>
|
310 |
+
<img src="/assets/dev2.png" alt="Member Photo" class="member-photo">
|
311 |
+
<div class="card-body">
|
312 |
+
<div class="contact-info">
|
313 |
+
<div class="contact-item">
|
314 |
+
<div class="contact-icon">
|
315 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
316 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
317 |
+
<circle cx="12" cy="7" r="4"></circle>
|
318 |
+
</svg>
|
319 |
+
</div>
|
320 |
+
<div class="contact-text">
|
321 |
+
<strong style="color: #fcee0a;">Name: </strong>
|
322 |
+
<sub style="margin-left: 10px;">Sai Darahas</sub>
|
323 |
+
</div>
|
324 |
+
</div>
|
325 |
+
<div class="contact-item">
|
326 |
+
<div class="contact-icon">
|
327 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
328 |
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
329 |
+
</svg>
|
330 |
+
</div>
|
331 |
+
<div class="contact-text">
|
332 |
+
<strong style="color: #fcee0a;">Phone</strong>
|
333 |
+
<sub style="margin-left: 10px;">+91-852147963</sub>
|
334 |
+
</div>
|
335 |
+
</div>
|
336 |
+
<div class="contact-item">
|
337 |
+
<div class="contact-icon">
|
338 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
339 |
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
340 |
+
<polyline points="22,6 12,13 2,6"></polyline>
|
341 |
+
</svg>
|
342 |
+
</div>
|
343 |
+
<div class="contact-text">
|
344 |
+
<strong style="color: #fcee0a;">Email</strong>
|
345 |
+
<sub style="margin-left: 10px;">[email protected]</sub>
|
346 |
+
</div>
|
347 |
+
</div>
|
348 |
+
</div>
|
349 |
+
<div class="social-links">
|
350 |
+
<a href="#" class="social-link">
|
351 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
352 |
+
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
|
353 |
+
</svg>
|
354 |
+
</a>
|
355 |
+
<a href="#" class="social-link">
|
356 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
357 |
+
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
358 |
+
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
359 |
+
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
360 |
+
</svg>
|
361 |
+
</a>
|
362 |
+
<a href="#" class="social-link">
|
363 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
364 |
+
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
|
365 |
+
</svg>
|
366 |
+
</a>
|
367 |
+
<a href="#" class="social-link">
|
368 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
369 |
+
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
|
370 |
+
<rect x="2" y="9" width="4" height="12"></rect>
|
371 |
+
<circle cx="4" cy="4" r="2"></circle>
|
372 |
+
</svg>
|
373 |
+
</a>
|
374 |
+
</div>
|
375 |
+
</div>
|
376 |
+
</div>
|
377 |
+
|
378 |
+
<!-- Card 3 -->
|
379 |
+
<div class="card">
|
380 |
+
<div class="card-number">03</div>
|
381 |
+
<img src="/assets/dev3.png" alt="Member Photo" class="member-photo">
|
382 |
+
<div class="card-body">
|
383 |
+
<div class="contact-info">
|
384 |
+
<div class="contact-item">
|
385 |
+
<div class="contact-icon">
|
386 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
387 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
388 |
+
<circle cx="12" cy="7" r="4"></circle>
|
389 |
+
</svg>
|
390 |
+
</div>
|
391 |
+
<div class="contact-text">
|
392 |
+
<strong style="color: #fcee0a;">Name: </strong>
|
393 |
+
<sub style="margin-left: 10px;">Thota Abilash Reddy</sub>
|
394 |
+
</div>
|
395 |
+
</div>
|
396 |
+
<div class="contact-item">
|
397 |
+
<div class="contact-icon">
|
398 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
399 |
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
400 |
+
</svg>
|
401 |
+
</div>
|
402 |
+
<div class="contact-text">
|
403 |
+
<strong style="color: #fcee0a;">Phone</strong>
|
404 |
+
<sub style="margin-left: 10px;">+91-7412589635</sub>
|
405 |
+
</div>
|
406 |
+
</div>
|
407 |
+
<div class="contact-item">
|
408 |
+
<div class="contact-icon">
|
409 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
410 |
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
411 |
+
<polyline points="22,6 12,13 2,6"></polyline>
|
412 |
+
</svg>
|
413 |
+
</div>
|
414 |
+
<div class="contact-text">
|
415 |
+
<strong style="color: #fcee0a;">Email</strong>
|
416 |
+
<sub style="margin-left: 10px;">[email protected]</sub>
|
417 |
+
</div>
|
418 |
+
</div>
|
419 |
+
</div>
|
420 |
+
<div class="social-links">
|
421 |
+
<a href="#" class="social-link">
|
422 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
423 |
+
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
|
424 |
+
</svg>
|
425 |
+
</a>
|
426 |
+
<a href="#" class="social-link">
|
427 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
428 |
+
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
429 |
+
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
430 |
+
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
431 |
+
</svg>
|
432 |
+
</a>
|
433 |
+
<a href="#" class="social-link">
|
434 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
435 |
+
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
|
436 |
+
</svg>
|
437 |
+
</a>
|
438 |
+
<a href="#" class="social-link">
|
439 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
440 |
+
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
|
441 |
+
<rect x="2" y="9" width="4" height="12"></rect>
|
442 |
+
<circle cx="4" cy="4" r="2"></circle>
|
443 |
+
</svg>
|
444 |
+
</a>
|
445 |
+
</div>
|
446 |
+
</div>
|
447 |
+
</div>
|
448 |
+
</div>
|
449 |
+
</div>
|
450 |
+
</body>
|
451 |
+
</html>
|
templates/feedback.html
ADDED
@@ -0,0 +1,566 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Feedback Form</title>
|
7 |
+
<!-- Added FontAwesome library for icons -->
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
9 |
+
<style>
|
10 |
+
@import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');
|
11 |
+
/* Global styles */
|
12 |
+
* {
|
13 |
+
margin: 0;
|
14 |
+
padding: 0;
|
15 |
+
box-sizing: border-box;
|
16 |
+
font-family: 'Poppins', 'Segoe UI', Arial, sans-serif;
|
17 |
+
}
|
18 |
+
|
19 |
+
body {
|
20 |
+
background-color: #333333;
|
21 |
+
color: #ffffff;
|
22 |
+
padding: 20px;
|
23 |
+
}
|
24 |
+
|
25 |
+
/* Header styles */
|
26 |
+
.header {
|
27 |
+
background-color: #ffed00;
|
28 |
+
padding: 30px 20px;
|
29 |
+
text-align: center;
|
30 |
+
color: #333333;
|
31 |
+
|
32 |
+
}
|
33 |
+
|
34 |
+
.header h1 {
|
35 |
+
font-size: 2.5rem;
|
36 |
+
font-weight: 800;
|
37 |
+
text-transform: uppercase;
|
38 |
+
letter-spacing: 1px;
|
39 |
+
text-shadow: 3px 3px 0 #333333;
|
40 |
+
margin-bottom: 10px;
|
41 |
+
font-family: 'Bungee Shade';
|
42 |
+
}
|
43 |
+
|
44 |
+
.header p {
|
45 |
+
font-size: 1rem;
|
46 |
+
margin-top: 15px;
|
47 |
+
font-weight: 500;
|
48 |
+
text-transform: uppercase;
|
49 |
+
letter-spacing: 1px;
|
50 |
+
font-family:'Courier New', Courier, monospace;
|
51 |
+
font-weight: bolder;
|
52 |
+
}
|
53 |
+
|
54 |
+
.header-underline {
|
55 |
+
width: 150px;
|
56 |
+
height: 5px;
|
57 |
+
background-color: #00b7ff;
|
58 |
+
margin: 5px auto 15px;
|
59 |
+
border-radius: 5px;
|
60 |
+
}
|
61 |
+
|
62 |
+
/* Form styles */
|
63 |
+
.form-container {
|
64 |
+
padding: 30px;
|
65 |
+
}
|
66 |
+
|
67 |
+
.form-group {
|
68 |
+
margin-bottom: 25px;
|
69 |
+
}
|
70 |
+
|
71 |
+
.form-group label {
|
72 |
+
display: block;
|
73 |
+
margin-bottom: 8px;
|
74 |
+
font-weight: 600;
|
75 |
+
color: #ffed00;
|
76 |
+
}
|
77 |
+
|
78 |
+
.form-control {
|
79 |
+
width: 100%;
|
80 |
+
padding: 12px 15px;
|
81 |
+
background-color: #3a3a3a;
|
82 |
+
border: 2px solid #ffed00;
|
83 |
+
border-radius: 8px;
|
84 |
+
color: #ffffff;
|
85 |
+
font-size: 16px;
|
86 |
+
}
|
87 |
+
|
88 |
+
.form-control:focus {
|
89 |
+
outline: none;
|
90 |
+
border-color: #00b7ff;
|
91 |
+
box-shadow: 0 0 5px rgba(0, 183, 255, 0.5);
|
92 |
+
}
|
93 |
+
|
94 |
+
textarea.form-control {
|
95 |
+
min-height: 120px;
|
96 |
+
resize: vertical;
|
97 |
+
}
|
98 |
+
|
99 |
+
.rating {
|
100 |
+
display: flex;
|
101 |
+
gap: 5px;
|
102 |
+
margin-top: 10px;
|
103 |
+
}
|
104 |
+
|
105 |
+
.rating input[type="radio"] {
|
106 |
+
display: none;
|
107 |
+
}
|
108 |
+
|
109 |
+
.rating label {
|
110 |
+
cursor: pointer;
|
111 |
+
width: 30px;
|
112 |
+
height: 30px;
|
113 |
+
background-color: #3a3a3a;
|
114 |
+
border: 2px solid #ffed00;
|
115 |
+
border-radius: 50%;
|
116 |
+
display: flex;
|
117 |
+
align-items: center;
|
118 |
+
justify-content: center;
|
119 |
+
color: #ffed00;
|
120 |
+
font-weight: bold;
|
121 |
+
transition: all 0.2s ease;
|
122 |
+
}
|
123 |
+
|
124 |
+
.rating input[type="radio"]:checked + label {
|
125 |
+
background-color: #ffed00;
|
126 |
+
color: #333333;
|
127 |
+
transform: scale(1.1);
|
128 |
+
}
|
129 |
+
|
130 |
+
.submit-btn {
|
131 |
+
display: block;
|
132 |
+
width: 100%;
|
133 |
+
padding: 15px;
|
134 |
+
background-color: #ffed00;
|
135 |
+
color: #333333;
|
136 |
+
border: none;
|
137 |
+
border-radius: 8px;
|
138 |
+
font-size: 18px;
|
139 |
+
font-weight: 700;
|
140 |
+
text-transform: uppercase;
|
141 |
+
cursor: pointer;
|
142 |
+
transition: all 0.3s ease;
|
143 |
+
margin-top: 10px;
|
144 |
+
}
|
145 |
+
|
146 |
+
|
147 |
+
|
148 |
+
.submit-btn:hover {
|
149 |
+
background-color: #00b7ff;
|
150 |
+
transform: translateY(-3px);
|
151 |
+
}
|
152 |
+
|
153 |
+
|
154 |
+
|
155 |
+
.form-section-title {
|
156 |
+
display: flex;
|
157 |
+
align-items: center;
|
158 |
+
margin-bottom: 20px;
|
159 |
+
}
|
160 |
+
|
161 |
+
.section-number {
|
162 |
+
width: 40px;
|
163 |
+
height: 40px;
|
164 |
+
background-color: #ffed00;
|
165 |
+
color: #333333;
|
166 |
+
border-radius: 50%;
|
167 |
+
display: flex;
|
168 |
+
align-items: center;
|
169 |
+
justify-content: center;
|
170 |
+
font-weight: bold;
|
171 |
+
font-size: 18px;
|
172 |
+
margin-right: 15px;
|
173 |
+
}
|
174 |
+
|
175 |
+
.section-title {
|
176 |
+
font-size: 1.3rem;
|
177 |
+
color: #ffed00;
|
178 |
+
}
|
179 |
+
|
180 |
+
|
181 |
+
.paper-alert {
|
182 |
+
background: #fff;
|
183 |
+
border-radius: 2px;
|
184 |
+
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23),
|
185 |
+
0 0 0 1px rgba(0,0,0,0.05);
|
186 |
+
min-width: 320px;
|
187 |
+
max-width: 400px;
|
188 |
+
position: fixed;
|
189 |
+
right: 16px;
|
190 |
+
top: 16px;
|
191 |
+
overflow: hidden;
|
192 |
+
opacity: 0;
|
193 |
+
pointer-events: none;
|
194 |
+
z-index: 10000;
|
195 |
+
transform-origin: top right;
|
196 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
197 |
+
}
|
198 |
+
|
199 |
+
/* Torn paper effect */
|
200 |
+
.paper-alert::before {
|
201 |
+
content: '';
|
202 |
+
position: absolute;
|
203 |
+
left: 0;
|
204 |
+
bottom: -2px;
|
205 |
+
width: 100%;
|
206 |
+
height: 4px;
|
207 |
+
background-color: #fff;
|
208 |
+
background-image:
|
209 |
+
radial-gradient(circle at 2px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
210 |
+
radial-gradient(circle at 10px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
211 |
+
radial-gradient(circle at 18px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
212 |
+
radial-gradient(circle at 26px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
213 |
+
radial-gradient(circle at 34px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px),
|
214 |
+
radial-gradient(circle at 42px 3px, transparent 2px, #fff 2px, #fff 3px, transparent 3px);
|
215 |
+
background-size: 50px 10px;
|
216 |
+
z-index: 2;
|
217 |
+
}
|
218 |
+
|
219 |
+
/* Paper texture */
|
220 |
+
.paper-alert::after {
|
221 |
+
content: '';
|
222 |
+
position: absolute;
|
223 |
+
left: 0;
|
224 |
+
top: 0;
|
225 |
+
width: 100%;
|
226 |
+
height: 100%;
|
227 |
+
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeBlend mode='screen'/%3E%3C/filter%3E%3Crect width='100' height='100' filter='url(%23noise)' opacity='0.08'/%3E%3C/svg%3E");
|
228 |
+
pointer-events: none;
|
229 |
+
z-index: 1;
|
230 |
+
}
|
231 |
+
|
232 |
+
.paper-alert.showAlert {
|
233 |
+
opacity: 1;
|
234 |
+
pointer-events: auto;
|
235 |
+
}
|
236 |
+
|
237 |
+
.paper-alert.show {
|
238 |
+
animation: paper_enter 0.7s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
|
239 |
+
}
|
240 |
+
|
241 |
+
@keyframes paper_enter {
|
242 |
+
0% { transform: translateX(100%) rotate(5deg); }
|
243 |
+
50% { transform: translateX(-10%) rotate(-2deg); }
|
244 |
+
75% { transform: translateX(5%) rotate(1deg); }
|
245 |
+
100% { transform: translateX(0) rotate(0); }
|
246 |
+
}
|
247 |
+
|
248 |
+
.paper-alert.hide {
|
249 |
+
animation: paper_exit 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
|
250 |
+
}
|
251 |
+
|
252 |
+
@keyframes paper_exit {
|
253 |
+
0% { transform: translateX(0) rotate(0); }
|
254 |
+
30% { transform: translateX(5%) rotate(1deg); }
|
255 |
+
100% { transform: translateX(110%) rotate(5deg); }
|
256 |
+
}
|
257 |
+
|
258 |
+
.paper-alert-content {
|
259 |
+
position: relative;
|
260 |
+
padding: 18px 15px;
|
261 |
+
z-index: 3;
|
262 |
+
}
|
263 |
+
|
264 |
+
.paper-pin {
|
265 |
+
position: absolute;
|
266 |
+
width: 12px;
|
267 |
+
height: 12px;
|
268 |
+
background: #f44336;
|
269 |
+
border-radius: 50%;
|
270 |
+
top: 10px;
|
271 |
+
left: 20px;
|
272 |
+
box-shadow: inset 0 0 0 2px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.2);
|
273 |
+
}
|
274 |
+
|
275 |
+
.paper-pin:after {
|
276 |
+
content: '';
|
277 |
+
position: absolute;
|
278 |
+
width: 6px;
|
279 |
+
height: 6px;
|
280 |
+
background: rgba(255,255,255,0.4);
|
281 |
+
border-radius: 50%;
|
282 |
+
top: 2px;
|
283 |
+
left: 2px;
|
284 |
+
}
|
285 |
+
|
286 |
+
.paper-clip {
|
287 |
+
position: absolute;
|
288 |
+
top: -8px;
|
289 |
+
right: 30px;
|
290 |
+
width: 30px;
|
291 |
+
height: 30px;
|
292 |
+
border: 2px solid #5d9cec;
|
293 |
+
border-radius: 0 0 0 10px;
|
294 |
+
border-top: none;
|
295 |
+
border-right: none;
|
296 |
+
transform: rotate(45deg);
|
297 |
+
z-index: 10;
|
298 |
+
}
|
299 |
+
|
300 |
+
.paper-clip:before {
|
301 |
+
content: '';
|
302 |
+
position: absolute;
|
303 |
+
width: 15px;
|
304 |
+
height: 10px;
|
305 |
+
border: 2px solid #5d9cec;
|
306 |
+
border-radius: 0 5px 0 5px;
|
307 |
+
border-top: none;
|
308 |
+
border-right: none;
|
309 |
+
top: 0;
|
310 |
+
left: 0;
|
311 |
+
transform: rotate(0deg);
|
312 |
+
}
|
313 |
+
|
314 |
+
.alert-icon {
|
315 |
+
display: flex;
|
316 |
+
align-items: center;
|
317 |
+
margin-bottom: 10px;
|
318 |
+
}
|
319 |
+
|
320 |
+
.alert-icon i {
|
321 |
+
font-size: 22px;
|
322 |
+
color: #f44336;
|
323 |
+
margin-right: 10px;
|
324 |
+
}
|
325 |
+
|
326 |
+
.alert-title {
|
327 |
+
font-weight: 600;
|
328 |
+
font-size: 16px;
|
329 |
+
color: #333;
|
330 |
+
margin: 0;
|
331 |
+
}
|
332 |
+
|
333 |
+
.paper-alert .msg {
|
334 |
+
margin-top: 6px;
|
335 |
+
margin-left: 32px;
|
336 |
+
font-size: 14px;
|
337 |
+
color: #555;
|
338 |
+
line-height: 1.4;
|
339 |
+
}
|
340 |
+
|
341 |
+
.paper-alert .close-btn {
|
342 |
+
position: absolute;
|
343 |
+
top: 10px;
|
344 |
+
right: 10px;
|
345 |
+
width: 24px;
|
346 |
+
height: 24px;
|
347 |
+
display: flex;
|
348 |
+
align-items: center;
|
349 |
+
justify-content: center;
|
350 |
+
cursor: pointer;
|
351 |
+
background: rgba(0,0,0,0.05);
|
352 |
+
border-radius: 50%;
|
353 |
+
color: #666;
|
354 |
+
transition: all 0.2s;
|
355 |
+
z-index: 5;
|
356 |
+
}
|
357 |
+
|
358 |
+
.paper-alert .close-btn:hover {
|
359 |
+
background: rgba(0,0,0,0.1);
|
360 |
+
color: #333;
|
361 |
+
transform: rotate(90deg);
|
362 |
+
}
|
363 |
+
|
364 |
+
.alert-btn {
|
365 |
+
position: fixed;
|
366 |
+
top: 7rem;
|
367 |
+
right: 2.5rem;
|
368 |
+
z-index: 1201;
|
369 |
+
font-size: 0.9rem;
|
370 |
+
padding: 0.7rem 1.5rem;
|
371 |
+
border-radius: 5px;
|
372 |
+
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
373 |
+
color: white;
|
374 |
+
border: none;
|
375 |
+
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
|
376 |
+
cursor: pointer;
|
377 |
+
font-family: 'Segoe UI', sans-serif;
|
378 |
+
font-weight: 600;
|
379 |
+
letter-spacing: 0.5px;
|
380 |
+
transition: all 0.3s;
|
381 |
+
}
|
382 |
+
|
383 |
+
.alert-btn:hover {
|
384 |
+
transform: translateY(-3px) scale(1.03);
|
385 |
+
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.5);
|
386 |
+
}
|
387 |
+
|
388 |
+
.paper-lines {
|
389 |
+
position: absolute;
|
390 |
+
left: 0;
|
391 |
+
top: 0;
|
392 |
+
width: 100%;
|
393 |
+
height: 100%;
|
394 |
+
background-image:
|
395 |
+
linear-gradient(to bottom, transparent 0%, transparent 98%, #e0e0e0 99%, transparent 100%);
|
396 |
+
background-size: 100% 24px;
|
397 |
+
z-index: 0;
|
398 |
+
pointer-events: none;
|
399 |
+
}
|
400 |
+
|
401 |
+
.handwritten {
|
402 |
+
font-family: 'Segoe Script', 'Bradley Hand', cursive;
|
403 |
+
transform: rotate(-1deg);
|
404 |
+
color: #2c3e50;
|
405 |
+
}
|
406 |
+
</style>
|
407 |
+
</head>
|
408 |
+
<body>
|
409 |
+
<div class="paper-alert hide" id="paperAlert">
|
410 |
+
<div class="paper-pin"></div>
|
411 |
+
<div class="paper-clip"></div>
|
412 |
+
<div class="paper-lines"></div>
|
413 |
+
<div class="paper-alert-content">
|
414 |
+
<div class="alert-icon">
|
415 |
+
<i class="fas fa-exclamation-circle" id="alertIcon"></i>
|
416 |
+
<h4 class="alert-title" id="alertTitle">Authentication Required</h4>
|
417 |
+
</div>
|
418 |
+
<div class="msg handwritten" id="alertMessage">You must be Logged In to continue!</div>
|
419 |
+
<div class="close-btn">
|
420 |
+
<i class="fas fa-times"></i>
|
421 |
+
</div>
|
422 |
+
</div>
|
423 |
+
</div>
|
424 |
+
|
425 |
+
<div class="header">
|
426 |
+
<h1>Customer Feedback</h1>
|
427 |
+
<div class="header-underline"></div>
|
428 |
+
<p>Help us improve our service by sharing your experience</p>
|
429 |
+
</div>
|
430 |
+
|
431 |
+
<form class="form-container" id="feedbackForm">
|
432 |
+
<div class="form-section-title">
|
433 |
+
<div class="section-number">01</div>
|
434 |
+
<h2 class="section-title">Your Information</h2>
|
435 |
+
</div>
|
436 |
+
|
437 |
+
<div class="form-group">
|
438 |
+
<label for="name">Full Name</label>
|
439 |
+
<input type="text" id="name" class="form-control" placeholder="Enter your name" required>
|
440 |
+
</div>
|
441 |
+
|
442 |
+
<div class="form-group">
|
443 |
+
<label for="email">Email Address</label>
|
444 |
+
<input type="email" id="email" class="form-control" placeholder="Enter your email" required>
|
445 |
+
</div>
|
446 |
+
|
447 |
+
<div class="form-group">
|
448 |
+
<label for="phone">Phone Number</label>
|
449 |
+
<input type="tel" id="phone" class="form-control" placeholder="Enter your phone number">
|
450 |
+
</div>
|
451 |
+
|
452 |
+
<div class="form-section-title">
|
453 |
+
<div class="section-number">02</div>
|
454 |
+
<h2 class="section-title">Your Experience</h2>
|
455 |
+
</div>
|
456 |
+
|
457 |
+
<div class="form-group">
|
458 |
+
<label>How would you rate your overall experience?</label>
|
459 |
+
<div class="rating">
|
460 |
+
<input type="radio" name="rating" id="star1" value="1">
|
461 |
+
<label for="star1">1</label>
|
462 |
+
|
463 |
+
<input type="radio" name="rating" id="star2" value="2">
|
464 |
+
<label for="star2">2</label>
|
465 |
+
|
466 |
+
<input type="radio" name="rating" id="star3" value="3">
|
467 |
+
<label for="star3">3</label>
|
468 |
+
|
469 |
+
<input type="radio" name="rating" id="star4" value="4">
|
470 |
+
<label for="star4">4</label>
|
471 |
+
|
472 |
+
<input type="radio" name="rating" id="star5" value="5">
|
473 |
+
<label for="star5">5</label>
|
474 |
+
</div>
|
475 |
+
</div>
|
476 |
+
|
477 |
+
<div class="form-section-title">
|
478 |
+
<div class="section-number">03</div>
|
479 |
+
<h2 class="section-title">Your Feedback</h2>
|
480 |
+
</div>
|
481 |
+
|
482 |
+
<div class="form-group">
|
483 |
+
<label for="comments">Please share your thoughts or suggestions</label>
|
484 |
+
<textarea id="comments" class="form-control" placeholder="Your feedback is valuable to us..." required></textarea>
|
485 |
+
</div>
|
486 |
+
|
487 |
+
<div class="form-group">
|
488 |
+
<label for="improvements">How can we improve our service?</label>
|
489 |
+
<textarea id="improvements" class="form-control" placeholder="Any suggestions for improvement..."></textarea>
|
490 |
+
</div>
|
491 |
+
|
492 |
+
<button type="submit" class="submit-btn" id="submitBtn">Submit Feedback</button>
|
493 |
+
</form>
|
494 |
+
|
495 |
+
|
496 |
+
<script>
|
497 |
+
// Wait for DOM to be fully loaded
|
498 |
+
document.addEventListener('DOMContentLoaded', function() {
|
499 |
+
console.log("DOM fully loaded");
|
500 |
+
|
501 |
+
// Get the form element with ID
|
502 |
+
const form = document.getElementById('feedbackForm');
|
503 |
+
console.log("Form element:", form);
|
504 |
+
|
505 |
+
// Function to show the paper alert
|
506 |
+
function showPaperAlert(message, title = "Alert", icon = "fa-exclamation-circle", duration = 7000) {
|
507 |
+
console.log("Showing alert:", message);
|
508 |
+
const paperAlert = document.getElementById('paperAlert');
|
509 |
+
const alertMessage = document.getElementById('alertMessage');
|
510 |
+
const alertTitle = document.getElementById('alertTitle');
|
511 |
+
const alertIcon = document.getElementById('alertIcon');
|
512 |
+
|
513 |
+
// Set custom message, title and icon
|
514 |
+
alertMessage.textContent = message;
|
515 |
+
alertTitle.textContent = title;
|
516 |
+
|
517 |
+
// Update the icon class
|
518 |
+
alertIcon.className = ''; // Clear existing classes
|
519 |
+
alertIcon.classList.add('fas', icon);
|
520 |
+
|
521 |
+
// Show the alert
|
522 |
+
paperAlert.classList.remove("hide");
|
523 |
+
paperAlert.classList.add("show", "showAlert");
|
524 |
+
|
525 |
+
// Auto-hide after specified duration
|
526 |
+
setTimeout(() => {
|
527 |
+
hideAlert();
|
528 |
+
}, duration);
|
529 |
+
}
|
530 |
+
|
531 |
+
// Function to hide the alert with animation
|
532 |
+
function hideAlert() {
|
533 |
+
console.log("Hiding alert");
|
534 |
+
const paperAlert = document.getElementById('paperAlert');
|
535 |
+
paperAlert.classList.remove("show");
|
536 |
+
paperAlert.classList.add("hide");
|
537 |
+
setTimeout(() => {
|
538 |
+
paperAlert.classList.remove("showAlert");
|
539 |
+
}, 500);
|
540 |
+
}
|
541 |
+
|
542 |
+
// Set up close button functionality
|
543 |
+
document.querySelector('#paperAlert .close-btn').addEventListener('click', function() {
|
544 |
+
hideAlert();
|
545 |
+
});
|
546 |
+
|
547 |
+
|
548 |
+
// Add event listener for form submission
|
549 |
+
if (form) {
|
550 |
+
form.addEventListener('submit', function(event) {
|
551 |
+
event.preventDefault();
|
552 |
+
console.log("Form submitted");
|
553 |
+
showPaperAlert(
|
554 |
+
"Thank you for your feedback!",
|
555 |
+
"Submission Successful",
|
556 |
+
"fa-check-circle",
|
557 |
+
7000
|
558 |
+
);
|
559 |
+
});
|
560 |
+
} else {
|
561 |
+
console.error("Form element not found!");
|
562 |
+
}
|
563 |
+
});
|
564 |
+
</script>
|
565 |
+
</body>
|
566 |
+
</html>
|
templates/gra.html
ADDED
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Interactive Citation Network</title>
|
7 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
8 |
+
<link rel="stylesheet" href="/static/css/gra.css">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div id="container"></div>
|
12 |
+
|
13 |
+
<!-- Replace the file-input div with this loading section -->
|
14 |
+
<div class="loading-section">
|
15 |
+
<div id="statusMessage">Loading citation network...</div>
|
16 |
+
<div class="progress-container">
|
17 |
+
<div id="loadingProgress" class="progress-bar"></div>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
|
21 |
+
<div class="time-navigation">
|
22 |
+
<button id="prevTimeButton" disabled>← Previous</button>
|
23 |
+
<div class="time-slider-container">
|
24 |
+
<input type="range" id="timeSlider" class="time-slider" min="0" max="0" value="0" step="1">
|
25 |
+
<div class="time-labels">
|
26 |
+
<span id="startTimeLabel">Start Date</span>
|
27 |
+
<span id="endTimeLabel">End Date</span>
|
28 |
+
</div>
|
29 |
+
<div id="timeLoader" class="time-loader"></div>
|
30 |
+
</div>
|
31 |
+
<div class="time-indicator" id="currentTimePeriod">Loading time range...</div>
|
32 |
+
<button id="nextTimeButton">Next →</button>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
|
36 |
+
<div class="controls">
|
37 |
+
<div class="control-section">
|
38 |
+
<h4>Network Physics</h4>
|
39 |
+
<label>
|
40 |
+
Link Distance:
|
41 |
+
<input type="range" id="linkDistanceSlider" min="20" max="200" value="80">
|
42 |
+
<span id="linkDistanceValue">80</span>
|
43 |
+
</label>
|
44 |
+
<label>
|
45 |
+
Charge Strength:
|
46 |
+
<input type="range" id="chargeSlider" min="-500" max="-10" value="-120">
|
47 |
+
<span id="chargeValue">-120</span>
|
48 |
+
</label>
|
49 |
+
<label>
|
50 |
+
Network Compactness:
|
51 |
+
<input type="range" id="gravitySlider" min="0.01" max="0.5" step="0.01" value="0.1">
|
52 |
+
<span id="gravityValue">0.1</span>
|
53 |
+
</label>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<div class="control-section">
|
57 |
+
<h4>Node Sizing</h4>
|
58 |
+
<label>
|
59 |
+
Main Papers:
|
60 |
+
<input type="range" id="mainNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
|
61 |
+
<span id="mainNodeScaleValue">1.0</span>
|
62 |
+
</label>
|
63 |
+
<label>
|
64 |
+
Bridge Papers:
|
65 |
+
<input type="range" id="bridgeNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
|
66 |
+
<span id="bridgeNodeScaleValue">1.0</span>
|
67 |
+
</label>
|
68 |
+
<label>
|
69 |
+
Normal Papers:
|
70 |
+
<input type="range" id="normalNodeScaleSlider" min="0.5" max="3.0" step="0.1" value="1.0">
|
71 |
+
<span id="normalNodeScaleValue">1.0</span>
|
72 |
+
</label>
|
73 |
+
</div>
|
74 |
+
|
75 |
+
<div class="control-section">
|
76 |
+
<h4>View Controls</h4>
|
77 |
+
<button id="resetButton">Reset View</button>
|
78 |
+
<button id="centerViewButton">Center View</button>
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
|
84 |
+
<div class="legend">
|
85 |
+
<h3>Legend</h3>
|
86 |
+
<div class="legend-item">
|
87 |
+
<div class="legend-indicator">
|
88 |
+
<div class="legend-circle main"></div>
|
89 |
+
</div>
|
90 |
+
<span>Main Paper (Circle)</span>
|
91 |
+
</div>
|
92 |
+
<div class="legend-item">
|
93 |
+
<div class="legend-indicator legend-star">
|
94 |
+
<svg width="20" height="20" viewBox="0 0 24 24">
|
95 |
+
<path d="M12,5 L13.25,9.25 L17.5,9.25 L14.12,11.75 L15.25,16 L12,13.5 L8.75,16 L9.88,11.75 L6.5,9.25 L10.75,9.25 Z" fill="#4285f4"></path>
|
96 |
+
</svg>
|
97 |
+
</div>
|
98 |
+
<span>Main Bridge Paper (Star)</span>
|
99 |
+
</div>
|
100 |
+
<div class="legend-item">
|
101 |
+
<div class="legend-indicator">
|
102 |
+
<div class="legend-circle referenced"></div>
|
103 |
+
</div>
|
104 |
+
<span>Referenced Paper (Circle)</span>
|
105 |
+
</div>
|
106 |
+
<div class="legend-item">
|
107 |
+
<div class="legend-indicator">
|
108 |
+
<div class="legend-circle citing"></div>
|
109 |
+
</div>
|
110 |
+
<span>Citing Paper (Circle)</span>
|
111 |
+
</div>
|
112 |
+
<div class="legend-item">
|
113 |
+
<div class="legend-indicator">
|
114 |
+
<div class="legend-diamond"></div>
|
115 |
+
</div>
|
116 |
+
<span>Bridge Paper (Diamond)</span>
|
117 |
+
</div>
|
118 |
+
<div class="legend-info">
|
119 |
+
<p>Node size also reflects importance:</p>
|
120 |
+
<p>• Main papers: Size may increase with citation count</p>
|
121 |
+
<p>• All node types can be scaled using the controls</p>
|
122 |
+
</div>
|
123 |
+
<div class="legend-item">
|
124 |
+
<div class="legend-indicator legend-arrow">
|
125 |
+
<svg width="50" height="20">
|
126 |
+
<defs>
|
127 |
+
<marker id="legend-arrow" viewBox="0 -5 10 10" refX="8" refY="0"
|
128 |
+
markerWidth="6" markerHeight="6" orient="auto">
|
129 |
+
<path d="M0,-5L10,0L0,5" fill="#999"></path>
|
130 |
+
</marker>
|
131 |
+
</defs>
|
132 |
+
<line x1="5" y1="10" x2="45" y2="10" stroke="#999" stroke-width="2" marker-end="url(#legend-arrow)"></line>
|
133 |
+
</svg>
|
134 |
+
</div>
|
135 |
+
<span>Citation Direction</span>
|
136 |
+
</div>
|
137 |
+
</div>
|
138 |
+
|
139 |
+
<!-- New Paper Details Sidebar -->
|
140 |
+
<div id="paperDetailsPanel" class="paper-details-panel">
|
141 |
+
<div class="panel-header">
|
142 |
+
<h3>Paper Details</h3>
|
143 |
+
<button id="closePanelButton" class="close-button">×</button>
|
144 |
+
</div>
|
145 |
+
<div class="panel-content">
|
146 |
+
<div id="paperInfo">
|
147 |
+
<h4 id="paperTitle">Select a paper to view details</h4>
|
148 |
+
<p id="paperType"></p>
|
149 |
+
<p id="paperCitations"></p>
|
150 |
+
<div id="paperConcepts"></div>
|
151 |
+
<div class="paper-link">
|
152 |
+
<a id="openAlexLink" href="#" target="_blank">View on OpenAlex</a>
|
153 |
+
</div>
|
154 |
+
</div>
|
155 |
+
|
156 |
+
<div class="paper-lists">
|
157 |
+
<div class="paper-list">
|
158 |
+
<h5>Citing Papers</h5>
|
159 |
+
<ul id="citingPapersList"></ul>
|
160 |
+
</div>
|
161 |
+
<div class="paper-list">
|
162 |
+
<h5>Referenced Papers</h5>
|
163 |
+
<ul id="referencedPapersList"></ul>
|
164 |
+
</div>
|
165 |
+
</div>
|
166 |
+
</div>
|
167 |
+
</div>
|
168 |
+
|
169 |
+
<!-- <div id="loadingIndicator">Loading data...</div> -->
|
170 |
+
|
171 |
+
<div class="tooltip" style="display: none;"></div>
|
172 |
+
|
173 |
+
<script type="text/javascript">
|
174 |
+
window.__INITIAL_DATA__= JSON.parse(`{{ data | safe }}`);
|
175 |
+
</script>
|
176 |
+
<script src="/static/js/gra.js"></script>
|
177 |
+
|
178 |
+
</body>
|
179 |
+
</html>
|
templates/homepage.html
ADDED
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>PaperLens</title>
|
7 |
+
<link rel="stylesheet" href="/static/css/homestyle.css">
|
8 |
+
<link rel="stylesheet" href="/static/css/profilepage.css">
|
9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
10 |
+
|
11 |
+
|
12 |
+
</head>
|
13 |
+
<body>
|
14 |
+
<div id="loading-overlay" class="loading-overlay">
|
15 |
+
<div class="loading-container">
|
16 |
+
<div class="cyclone" id="cyclone"></div>
|
17 |
+
<div class="ground"></div>
|
18 |
+
<div class="loading-text" id="loading-text">Loading...</div>
|
19 |
+
</div>
|
20 |
+
</div>
|
21 |
+
<div class="paper-alert hide" id="paperAlert">
|
22 |
+
<div class="paper-pin"></div>
|
23 |
+
<div class="paper-clip"></div>
|
24 |
+
<div class="paper-lines"></div>
|
25 |
+
<div class="paper-alert-content">
|
26 |
+
<div class="alert-icon">
|
27 |
+
<i class="fas fa-exclamation-circle" id="alertIcon"></i>
|
28 |
+
<h4 class="alert-title" id="alertTitle">Authentication Required</h4>
|
29 |
+
</div>
|
30 |
+
<div class="msg handwritten" id="alertMessage">You must be Logged In to continue!</div>
|
31 |
+
<div class="close-btn">
|
32 |
+
<i class="fas fa-times"></i>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
<header>
|
37 |
+
<div class="header-content">
|
38 |
+
<div class="logo-container">
|
39 |
+
<img src="/assets/logo.svg" style="height: 70px; width: auto;" alt="PaperLens Logo">
|
40 |
+
<h1 id="appname">PaperLens</h1>
|
41 |
+
</div>
|
42 |
+
<nav>
|
43 |
+
<a href="#interactive-book">Features</a>
|
44 |
+
<a href="#architecture ">Solutions</a>
|
45 |
+
<a href="#contacts">Contacts</a>
|
46 |
+
<a href="#feedback">Feedback</a>
|
47 |
+
<a href="/login"><button class="sign-in-btn" id="signInBtn">Sign In</button></a>
|
48 |
+
<a id="userProfile" class="profile-trigger">
|
49 |
+
<span id="userName">dara</span>
|
50 |
+
<img src="/assets/userPhoto.png" alt="Profile Picture" class="profile-pic" id="profile-pic">
|
51 |
+
<i class="fas fa-chevron-down"></i>
|
52 |
+
</a>
|
53 |
+
</nav>
|
54 |
+
</div>
|
55 |
+
</header>
|
56 |
+
|
57 |
+
<div class="profile-container" id="profileContainer">
|
58 |
+
<div class="profile-overlay" id="profileOverlay"></div>
|
59 |
+
<div class="profile-dropdown" id="profileDropdown">
|
60 |
+
<div class="profile-header">
|
61 |
+
<div class="profile-cover-photo"></div>
|
62 |
+
<div class="profile-avatar-container">
|
63 |
+
<img src="/assets/default-user.jpg" alt="Profile Picture" id="profileAvatar">
|
64 |
+
</div>
|
65 |
+
<h2 id="profileHeaderName">Welcome, Dara</h2>
|
66 |
+
</div>
|
67 |
+
|
68 |
+
<div class="profile-tabs">
|
69 |
+
<button class="tab-btn active" data-tab="personal">Personal Info</button>
|
70 |
+
<button class="tab-btn" data-tab="activity">Activity</button>
|
71 |
+
</div>
|
72 |
+
|
73 |
+
<div class="tab-content active" id="personal-tab">
|
74 |
+
<div class="profile-field">
|
75 |
+
<label><i class="fas fa-user"></i> Name</label>
|
76 |
+
<input type="text" id="profileName" placeholder="Enter your name" disabled>
|
77 |
+
<span class="edit-icon" onclick="enableEdit('profileName')" >
|
78 |
+
<img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
|
79 |
+
</div>
|
80 |
+
|
81 |
+
<div class="profile-field">
|
82 |
+
<label><i class="fas fa-envelope"></i> Email</label>
|
83 |
+
<input type="email" id="profileEmail" disabled>
|
84 |
+
<div class="verification-badge" id="emailVerified">
|
85 |
+
<i class="fas fa-check-circle"></i> Verified
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
|
89 |
+
<div class="profile-field">
|
90 |
+
<label><i class="fas fa-phone"></i> Phone No</label>
|
91 |
+
<input type="text" id="profilePhone" placeholder="Enter phone number" disabled>
|
92 |
+
<span class="edit-icon" onclick="enableEdit('profilePhone')">
|
93 |
+
<img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
|
94 |
+
</div>
|
95 |
+
|
96 |
+
<div class="profile-field">
|
97 |
+
<label><i class="fas fa-map-marker-alt"></i> Address</label>
|
98 |
+
<input type="text" id="profileAddress" placeholder="Enter address" disabled>
|
99 |
+
<span class="edit-icon" onclick="enableEdit('profileAddress')">
|
100 |
+
<img src="/assets/editicon.png" alt="Edit" class="edit-img">
|
101 |
+
</span>
|
102 |
+
</div>
|
103 |
+
|
104 |
+
<div class="profile-field">
|
105 |
+
<label><i class="fas fa-briefcase"></i> Profession</label>
|
106 |
+
<input type="text" id="profileProfession" placeholder="Enter profession" disabled>
|
107 |
+
<span class="edit-icon" onclick="enableEdit('profileProfession')">
|
108 |
+
<img src="/assets/editicon.png" alt="Edit" class="edit-img"></span>
|
109 |
+
</div>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
|
113 |
+
|
114 |
+
<div class="tab-content" id="activity-tab">
|
115 |
+
<div class="activity-summary">
|
116 |
+
<div class="activity-stat">
|
117 |
+
<i class="fas fa-search"></i>
|
118 |
+
<span class="stat-value">32</span>
|
119 |
+
<span class="stat-label">Searches</span>
|
120 |
+
</div>
|
121 |
+
</div>
|
122 |
+
|
123 |
+
|
124 |
+
|
125 |
+
<h3 class="section-title">Recent Activity</h3>
|
126 |
+
<div class="timeline">
|
127 |
+
<div class="timeline-item">
|
128 |
+
<div class="timeline-dot"></div>
|
129 |
+
<div class="timeline-content">
|
130 |
+
<h4>Research on Neural Networks</h4>
|
131 |
+
<p>You searched for papers on neural networks in medicine</p>
|
132 |
+
<span class="timeline-date"><i class="fas fa-clock"></i> 2 days ago</span>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
<div class="timeline-item">
|
136 |
+
<div class="timeline-dot"></div>
|
137 |
+
<div class="timeline-content">
|
138 |
+
<h4>Saved Article</h4>
|
139 |
+
<p>You saved "Emerging Trends in AI Ethics"</p>
|
140 |
+
<span class="timeline-date"><i class="fas fa-clock"></i> 1 week ago</span>
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
<div class="timeline-item">
|
144 |
+
<div class="timeline-dot"></div>
|
145 |
+
<div class="timeline-content">
|
146 |
+
<h4>Profile Updated</h4>
|
147 |
+
<p>You updated your professional information</p>
|
148 |
+
<span class="timeline-date"><i class="fas fa-clock"></i> 2 weeks ago</span>
|
149 |
+
</div>
|
150 |
+
</div>
|
151 |
+
</div>
|
152 |
+
</div>
|
153 |
+
|
154 |
+
<div class="action-buttons">
|
155 |
+
<button class="save-btn" id="saveBtn"><i class="fas fa-save"></i> Save Changes</button>
|
156 |
+
<button class="logout-btn" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> Logout</button>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
</div>
|
160 |
+
|
161 |
+
|
162 |
+
|
163 |
+
|
164 |
+
<section class="hero">
|
165 |
+
<div class="orb"></div>
|
166 |
+
<div class="orb"></div>
|
167 |
+
<div class="orb"></div>
|
168 |
+
<h2> Charting Research Trajectories and
|
169 |
+
Academic Linkages</h2>
|
170 |
+
<div class="liquid-container">
|
171 |
+
<div class="liquid-word active" id="word1">Innovative</div>
|
172 |
+
<div class="liquid-word" id="word2">Dynamic</div>
|
173 |
+
<div class="liquid-word" id="word3">Effortless</div>
|
174 |
+
<div class="splash-container" id="splashContainer"></div>
|
175 |
+
</div>
|
176 |
+
<div class="search-container">
|
177 |
+
<div class="searchBox">
|
178 |
+
<input id="searchy" class="searchInput"type="text" name="" placeholder="Search">
|
179 |
+
<div class="filter-container">
|
180 |
+
<select id="year-filter" class="year-filter">
|
181 |
+
<option value="">Year</option>
|
182 |
+
</select>
|
183 |
+
</div>
|
184 |
+
<img id="searchbtn" src="/assets/searchy.png" >
|
185 |
+
</div>
|
186 |
+
|
187 |
+
<!-- <button id="searchbtn">Search </button> -->
|
188 |
+
<button id="nextBtn" style="display: none;" >Next</button>
|
189 |
+
</div>
|
190 |
+
<div class="feature-selection-container">
|
191 |
+
<div class="feature-selection">
|
192 |
+
<div data-text="Trend Analysis" data-feature="trends" style="--r:-15;" class="glass">
|
193 |
+
<img src="/assets/trenzkj.png" alt="Trend Analysis" class="glass-icon" />
|
194 |
+
</div>
|
195 |
+
|
196 |
+
|
197 |
+
<div data-text="Citation Network" data-feature="citations" style="--r:-15;" class="glass">
|
198 |
+
<img src="/assets/icons8-hub-100.png" alt="Citation Network Analysis" class="glass-icon" />
|
199 |
+
</div>
|
200 |
+
<div data-text="Venue & Publisher Analysis" data-feature="venues" style="--r:-15;" class="glass">
|
201 |
+
<img src="/assets/trenzkj.png" alt="Venue & Publisher Analysis" class="glass-icon" />
|
202 |
+
</div>
|
203 |
+
</div>
|
204 |
+
<div class="selection-message" id="selectionMessage">
|
205 |
+
You've selected <span id="selectedFeature">Trend Analysis</span>. Click Search to continue.
|
206 |
+
</div>
|
207 |
+
</div>
|
208 |
+
</section>
|
209 |
+
|
210 |
+
<section id="interactive-book" class="interactive-book">
|
211 |
+
<h2 class="header-title">Features</h2>
|
212 |
+
<div class="carousel-container" id="carousel-container">
|
213 |
+
<!-- HTML Fix - Corrected slide 6 transform -->
|
214 |
+
<div class="carousel" id="carousel">
|
215 |
+
<div class="slide" style="transform: rotateY(0deg) translateZ(630px);">
|
216 |
+
<h4 class="card-title" style="color: aqua;font-family: 'Bungee Shade';">What Is Trend Analysis?</h4>
|
217 |
+
<p class="card-text" style="color:white; font:bolder">Trend Analysis via Topic Modeling in our project involves identifying and visualizing patterns in research papers for the given topic to uncover underlying themes and trends</p>
|
218 |
+
<h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
|
219 |
+
<p style="color: white;">Underlying themes thorugh clusters and themes and the system categorizes these topics into three distant importance.
|
220 |
+
- 🔥Hot topics ,🔄Neutraltopics ,💎Gap topics.
|
221 |
+
</p>
|
222 |
+
</div>
|
223 |
+
|
224 |
+
<div class="slide" style="transform: rotateY(60deg) translateZ(630px);">
|
225 |
+
<video src="/assets/d1.mp4" autoplay loop muted playsinline>
|
226 |
+
</div>
|
227 |
+
|
228 |
+
<div class="slide" style="transform: rotateY(120deg) translateZ(630px);">
|
229 |
+
<h4 style="color: aqua;font-family: 'Bungee Shade';">What is Citation Network</h4>
|
230 |
+
<p style="color: white;">This network maps relationships between research papers based on citations,
|
231 |
+
revealing influential works, clusters of related studies, and knowledge flow within a field.<br></p>
|
232 |
+
<h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
|
233 |
+
<ul style="color: white;">
|
234 |
+
<li>Identify Main Papers</li>
|
235 |
+
<li>Indentify Bridge Papers</li>
|
236 |
+
<li>Filter Reference And Cited Papers</li>
|
237 |
+
</ul>
|
238 |
+
</div>
|
239 |
+
|
240 |
+
<div class="slide" style="transform: rotateY(180deg) translateZ(630px);">
|
241 |
+
<video src="/assets/d2.mp4" autoplay loop muted playsinline>
|
242 |
+
</div>
|
243 |
+
|
244 |
+
<div class="slide" style="transform: rotateY(240deg) translateZ(630px);">
|
245 |
+
<h4 style="color: aqua;font-family: 'Bungee Shade';">What is Host/Publisher Analysis</h4>
|
246 |
+
<p style="color: white;">Venue and publisher analysis identifies key players in the research landscape,
|
247 |
+
such as institutions and journals, by visualizing clusters of host organizations or
|
248 |
+
publication venues based on their publication patterns and influence.
|
249 |
+
</p>
|
250 |
+
<h4 style="color: aqua;font-family: 'Bungee Shade';">What Does It Answers?</h4>
|
251 |
+
<ul style="display: flex; gap: 20px; padding: 0; color:white;">
|
252 |
+
<li>Status of Open To Access papers</li>
|
253 |
+
<li>Types of papers</li>
|
254 |
+
<li>Host and Publication Patterns</li>
|
255 |
+
</ul>
|
256 |
+
</div>
|
257 |
+
|
258 |
+
|
259 |
+
<div class="slide" style="transform: rotateY(300deg) translateZ(630px);">
|
260 |
+
<video src="/assets/d3.mp4" autoplay loop muted playsinline>
|
261 |
+
</div>
|
262 |
+
</div>
|
263 |
+
|
264 |
+
|
265 |
+
|
266 |
+
<div class="pagination">
|
267 |
+
<div class="dot active" data-index="0"></div>
|
268 |
+
<div class="dot" data-index="1"></div>
|
269 |
+
<div class="dot" data-index="2"></div>
|
270 |
+
<div class="dot" data-index="3"></div>
|
271 |
+
<div class="dot" data-index="4"></div>
|
272 |
+
<div class="dot" data-index="5"></div>
|
273 |
+
<div class="dot" data-index="5"></div>
|
274 |
+
</div>
|
275 |
+
</div>
|
276 |
+
|
277 |
+
<!-- Refined Open Book -->
|
278 |
+
<div class="book-container">
|
279 |
+
<div class="book">
|
280 |
+
<div class="book-left">
|
281 |
+
<div class="page-content">
|
282 |
+
<div class="page-line"></div>
|
283 |
+
<div class="page-line"></div>
|
284 |
+
<div class="page-line"></div>
|
285 |
+
</div>
|
286 |
+
<div class="page-fold"></div>
|
287 |
+
<div class="page-number page-number-left">1</div>
|
288 |
+
</div>
|
289 |
+
|
290 |
+
<div class="book-center"></div>
|
291 |
+
|
292 |
+
<div class="page" id="page1">
|
293 |
+
<div class="page-front">
|
294 |
+
<div class="page-content">
|
295 |
+
<div class="page-line"></div>
|
296 |
+
<div class="page-line"></div>
|
297 |
+
<div class="page-line"></div>
|
298 |
+
</div>
|
299 |
+
<div class="page-fold"></div>
|
300 |
+
<div class="page-number page-number-right">2</div>
|
301 |
+
</div>
|
302 |
+
<div class="page-back">
|
303 |
+
<div class="page-content">
|
304 |
+
<div class="page-line"></div>
|
305 |
+
<div class="page-line"></div>
|
306 |
+
<div class="page-line"></div>
|
307 |
+
</div>
|
308 |
+
<div class="page-fold"></div>
|
309 |
+
<div class="page-number page-number-left">3</div>
|
310 |
+
</div>
|
311 |
+
<div class="page-shadow"></div>
|
312 |
+
</div>
|
313 |
+
|
314 |
+
<div class="page" id="page2">
|
315 |
+
<div class="page-front">
|
316 |
+
<div class="page-content">
|
317 |
+
<div class="page-line"></div>
|
318 |
+
<div class="page-line"></div>
|
319 |
+
<div class="page-line"></div>
|
320 |
+
</div>
|
321 |
+
<div class="page-fold"></div>
|
322 |
+
<div class="page-number page-number-right">4</div>
|
323 |
+
</div>
|
324 |
+
<div class="page-back">
|
325 |
+
<div class="page-content">
|
326 |
+
<div class="page-line"></div>
|
327 |
+
<div class="page-line"></div>
|
328 |
+
<div class="page-line"></div>
|
329 |
+
</div>
|
330 |
+
<div class="page-fold"></div>
|
331 |
+
<div class="page-number page-number-left">5</div>
|
332 |
+
</div>
|
333 |
+
<div class="page-shadow"></div>
|
334 |
+
</div>
|
335 |
+
|
336 |
+
<div class="page" id="page3">
|
337 |
+
<div class="page-front">
|
338 |
+
<div class="page-content">
|
339 |
+
<div class="page-line"></div>
|
340 |
+
<div class="page-line"></div>
|
341 |
+
<div class="page-line"></div>
|
342 |
+
</div>
|
343 |
+
<div class="page-fold"></div>
|
344 |
+
<div class="page-number page-number-right">6</div>
|
345 |
+
</div>
|
346 |
+
<div class="page-back">
|
347 |
+
<div class="page-content">
|
348 |
+
<div class="page-line"></div>
|
349 |
+
<div class="page-line"></div>
|
350 |
+
<div class="page-line"></div>
|
351 |
+
</div>
|
352 |
+
<div class="page-fold"></div>
|
353 |
+
<div class="page-number page-number-left">7</div>
|
354 |
+
</div>
|
355 |
+
<div class="page-shadow"></div>
|
356 |
+
</div>
|
357 |
+
|
358 |
+
<div class="page" id="page4">
|
359 |
+
<div class="page-front">
|
360 |
+
<div class="page-content">
|
361 |
+
<div class="page-line"></div>
|
362 |
+
<div class="page-line"></div>
|
363 |
+
<div class="page-line"></div>
|
364 |
+
</div>
|
365 |
+
<div class="page-fold"></div>
|
366 |
+
<div class="page-number page-number-right">8</div>
|
367 |
+
</div>
|
368 |
+
<div class="page-back">
|
369 |
+
<div class="page-content">
|
370 |
+
<div class="page-line"></div>
|
371 |
+
<div class="page-line"></div>
|
372 |
+
<div class="page-line"></div>
|
373 |
+
</div>
|
374 |
+
<div class="page-fold"></div>
|
375 |
+
<div class="page-number page-number-left">9</div>
|
376 |
+
</div>
|
377 |
+
<div class="page-shadow"></div>
|
378 |
+
</div>
|
379 |
+
|
380 |
+
<div class="page" id="page5">
|
381 |
+
<div class="page-front">
|
382 |
+
<div class="page-content">
|
383 |
+
<div class="page-line"></div>
|
384 |
+
<div class="page-line"></div>
|
385 |
+
<div class="page-line"></div>
|
386 |
+
</div>
|
387 |
+
<div class="page-fold"></div>
|
388 |
+
<div class="page-number page-number-right">10</div>
|
389 |
+
</div>
|
390 |
+
<div class="page-back">
|
391 |
+
<div class="page-content">
|
392 |
+
<div class="page-line"></div>
|
393 |
+
<div class="page-line"></div>
|
394 |
+
<div class="page-line"></div>
|
395 |
+
</div>
|
396 |
+
<div class="page-fold"></div>
|
397 |
+
<div class="page-number page-number-left">11</div>
|
398 |
+
</div>
|
399 |
+
<div class="page-shadow"></div>
|
400 |
+
</div>
|
401 |
+
|
402 |
+
|
403 |
+
<div class="book-right">
|
404 |
+
<div class="page-content">
|
405 |
+
<div class="page-line"></div>
|
406 |
+
<div class="page-line"></div>
|
407 |
+
<div class="page-line"></div>
|
408 |
+
</div>
|
409 |
+
<div class="page-fold"></div>
|
410 |
+
<div class="page-number page-number-right">12</div>
|
411 |
+
</div>
|
412 |
+
|
413 |
+
<div class="book-shadow"></div>
|
414 |
+
</div>
|
415 |
+
|
416 |
+
<div class="navigation">
|
417 |
+
<button id="prevBtn">Previous</button>
|
418 |
+
<button id="nextBtn2">Next</button>
|
419 |
+
</div>
|
420 |
+
</div>
|
421 |
+
</section>
|
422 |
+
|
423 |
+
<section id="architecture" class="architecture">
|
424 |
+
<div class="architecture-description">
|
425 |
+
<h3>Our Innovative Research Architecture</h3>
|
426 |
+
<p>PaperLens architecture integrates data scraping from Semantic API and OpenAlex, followed by robust data cleaning, to enable three key features ,Trend Analysis, Citation Networks, and Host/venue insights through interactive, user-friendly dashboards.</p>
|
427 |
+
|
428 |
+
<div class="architecture-features">
|
429 |
+
<div class="architecture-feature">
|
430 |
+
<h4 style="text-align: center;">Dynamic Data Collection</h4>
|
431 |
+
<p>Continuously harvests research papers from IEEE, ArXiv, PMC, JSTOR, and PLOS databases by using semantic scholar and openAlex api's to ensure comprehensive coverage across disciplines. Say no to manual data loading.</p>
|
432 |
+
</div>
|
433 |
+
<div class="architecture-feature">
|
434 |
+
<h4 style="text-align: center;">Integrated Solution</h4>
|
435 |
+
<p>PaperLens integrates advanced science mapping features like trend analysis and citation networks into an intuitive platform, bridging gaps left by traditional tools that often require complex configurations or lack interactivity.</p>
|
436 |
+
</div>
|
437 |
+
<div class="architecture-feature">
|
438 |
+
<h4 style="text-align: center;">Ease Of Use </h4>
|
439 |
+
<p>Designed for simplicity, our tool allows users to type and search effortlessly, eliminating the need to learn heavy concepts, unlike many existing science mapping tools that demand extensive expertise for basic tasks.</p>
|
440 |
+
</div>
|
441 |
+
</div>
|
442 |
+
</div>
|
443 |
+
<div class="container">
|
444 |
+
<h2 style="margin-top: 30px;font-family: 'Bungee Shade', cursive;color:aqua">PaperLens</h2>
|
445 |
+
<svg class="paths-container">
|
446 |
+
</svg>
|
447 |
+
<div class="center-box">
|
448 |
+
<div class="settings-icon-wrapper">
|
449 |
+
<img src="/assets/setting.png" alt="Settings">
|
450 |
+
</div>
|
451 |
+
</div>
|
452 |
+
</div>
|
453 |
+
</section>
|
454 |
+
<section id="contacts" class="contacts">
|
455 |
+
<iframe src="/contact" frameborder="1" width="100%" height="800px" style="border: 8px solid #333; border-radius: 8px; margin: 20px 0;"></iframe>
|
456 |
+
</section>
|
457 |
+
|
458 |
+
<section id="feedback" class="feedback">
|
459 |
+
<iframe src="/feedback" frameborder="1" width="100%" height="1300px" style="border: 8px solid #333; border-radius: 8px; "></iframe>
|
460 |
+
</section>
|
461 |
+
|
462 |
+
<footer>
|
463 |
+
<p>© 2025 PaperLens. All rights reserved.</p>
|
464 |
+
</footer>
|
465 |
+
|
466 |
+
|
467 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-app-compat.js"></script>
|
468 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-auth-compat.js"></script>
|
469 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-firestore-compat.js"></script>
|
470 |
+
<script type="module" src="/static/js/home.js"></script>
|
471 |
+
<script type="module" src="/static/js/login.js"></script>
|
472 |
+
<script type="module" src="/static/js/useractivity.js"></script>
|
473 |
+
</body>
|
474 |
+
|
475 |
+
</html>
|
templates/loginpage.html
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Login & Signup</title>
|
7 |
+
<link rel="stylesheet" href="/static/css/loginstyle.css">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="paper-fall"></div>
|
12 |
+
<div class="wrapper">
|
13 |
+
<div class="form-container sign-up" id="seperate">
|
14 |
+
<form action="#">
|
15 |
+
<h2>sign up</h2>
|
16 |
+
<div class="form-group">
|
17 |
+
<input type="text" required>
|
18 |
+
<i class="fas fa-user"></i>
|
19 |
+
<label for="">name</label>
|
20 |
+
</div>
|
21 |
+
<div class="form-group">
|
22 |
+
<input type="email" required>
|
23 |
+
<i class="fas fa-at"></i>
|
24 |
+
<label for="">email</label>
|
25 |
+
</div>
|
26 |
+
<div class="form-group">
|
27 |
+
<input type="password" required>
|
28 |
+
<i class="fas fa-lock"></i>
|
29 |
+
<label for="">password</label>
|
30 |
+
</div>
|
31 |
+
<div class="form-group">
|
32 |
+
<input type="password" required>
|
33 |
+
<i class="fas fa-lock"></i>
|
34 |
+
<label for="">confirm password</label>
|
35 |
+
</div>
|
36 |
+
<button type="submit" class="btn">sign up</button>
|
37 |
+
<div class="link">
|
38 |
+
<p style="color: white;">You already have an account?<a href="#" class="signin-link"> sign in</a></p>
|
39 |
+
</div>
|
40 |
+
</form>
|
41 |
+
</div>
|
42 |
+
<div class="form-container sign-in">
|
43 |
+
<form>
|
44 |
+
<h2>login</h2>
|
45 |
+
<div class="form-group">
|
46 |
+
<input type="email" required>
|
47 |
+
<i class="fas fa-at"></i>
|
48 |
+
<label for="">email</label>
|
49 |
+
</div>
|
50 |
+
<div class="form-group">
|
51 |
+
<input type="password" required>
|
52 |
+
<i class="fas fa-lock"></i>
|
53 |
+
<label for="">password</label>
|
54 |
+
</div>
|
55 |
+
<div class="forgot-pass">
|
56 |
+
<a href="#">forgot password?</a>
|
57 |
+
</div>
|
58 |
+
<button type="submit" class="btn">login</button>
|
59 |
+
<div class="link">
|
60 |
+
<p style="color: white;">Don't have an account?<a href="#" class="signup-link"> sign up</a></p>
|
61 |
+
</div>
|
62 |
+
</form>
|
63 |
+
</div>
|
64 |
+
<div class="form-container forgot-password">
|
65 |
+
<form action="#">
|
66 |
+
<h2>forgot password</h2>
|
67 |
+
<div class="form-group">
|
68 |
+
<input type="email" required>
|
69 |
+
<i class="fas fa-at"></i>
|
70 |
+
<label for="">email</label>
|
71 |
+
</div>
|
72 |
+
<button type="submit" class="btn">submit</button>
|
73 |
+
<div class="link">
|
74 |
+
<p style="color: white;">Go to Login Page ? <a href="#" class="forget-link">login</a></p>
|
75 |
+
</div>
|
76 |
+
</form>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
|
80 |
+
<!-- Firebase Scripts -->
|
81 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-app-compat.js"></script>
|
82 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-auth-compat.js"></script>
|
83 |
+
<script src="https://www.gstatic.com/firebasejs/9.0.2/firebase-firestore-compat.js"></script>
|
84 |
+
<script src="/static/js/login.js"></script>
|
85 |
+
</body>
|
86 |
+
</html>
|
venuAnalysis.py
ADDED
@@ -0,0 +1,837 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import plotly.express as px
|
3 |
+
import plotly.graph_objects as go
|
4 |
+
from dash import Dash, dcc, html, Input, Output, State
|
5 |
+
import numpy as np
|
6 |
+
import random
|
7 |
+
import math
|
8 |
+
from collections import defaultdict
|
9 |
+
import colorsys
|
10 |
+
from fastapi import HTTPException
|
11 |
+
from pydantic import BaseModel
|
12 |
+
import threading
|
13 |
+
import webbrowser
|
14 |
+
import os
|
15 |
+
import psutil
|
16 |
+
import socket
|
17 |
+
from fastapi import HTTPException, APIRouter, Request
|
18 |
+
router = APIRouter()
|
19 |
+
|
20 |
+
# Global variables to track dashboard state
|
21 |
+
dashboard_port = 8050
|
22 |
+
dashboard_process = None
|
23 |
+
|
24 |
+
# MongoDB connection and data loader function
|
25 |
+
async def load_data_from_mongodb(userId, topic, year, request:Request):
|
26 |
+
query = {
|
27 |
+
"userId": userId,
|
28 |
+
"topic": topic,
|
29 |
+
"year": year
|
30 |
+
}
|
31 |
+
collection = request.app.state.collection2
|
32 |
+
document = await collection.find_one(query)
|
33 |
+
if not document:
|
34 |
+
raise ValueError(f"No data found for userId={userId}, topic={topic}, year={year}")
|
35 |
+
# Extract metadata and convert to DataFrame
|
36 |
+
metadata = document.get("metadata", [])
|
37 |
+
df = pd.DataFrame(metadata)
|
38 |
+
df['publication_date'] = pd.to_datetime(df['publication_date'])
|
39 |
+
return df
|
40 |
+
|
41 |
+
# Common functions (unchanged)
|
42 |
+
def filter_by_date_range(dataframe, start_idx, end_idx):
|
43 |
+
start_date = date_range[start_idx]
|
44 |
+
end_date = date_range[end_idx]
|
45 |
+
return dataframe[(dataframe['publication_date'] >= start_date) &
|
46 |
+
(dataframe['publication_date'] <= end_date)]
|
47 |
+
|
48 |
+
def generate_vibrant_colors(n):
|
49 |
+
base_colors = []
|
50 |
+
for i in range(n):
|
51 |
+
hue = (i / n) % 1.0
|
52 |
+
saturation = random.uniform(0.7, 0.9)
|
53 |
+
value = random.uniform(0.7, 0.9)
|
54 |
+
r, g, b = colorsys.hsv_to_rgb(hue, saturation, value)
|
55 |
+
vibrant_color = '#{:02x}{:02x}{:02x}'.format(
|
56 |
+
int(r * 255),
|
57 |
+
int(g * 255),
|
58 |
+
int(b * 255)
|
59 |
+
)
|
60 |
+
end_color_r = min(255, int(r * 255 * 1.1))
|
61 |
+
end_color_g = min(255, int(g * 255 * 1.1))
|
62 |
+
end_color_b = min(255, int(b * 255 * 1.1))
|
63 |
+
gradient_end = '#{:02x}{:02x}{:02x}'.format(end_color_r, end_color_g, end_color_b)
|
64 |
+
base_colors.append({
|
65 |
+
'start': vibrant_color,
|
66 |
+
'end': gradient_end
|
67 |
+
})
|
68 |
+
extended_colors = base_colors * math.ceil(n/10)
|
69 |
+
final_colors = []
|
70 |
+
for i in range(n):
|
71 |
+
color = extended_colors[i]
|
72 |
+
jitter = random.uniform(0.9, 1.1)
|
73 |
+
def jitter_color(hex_color):
|
74 |
+
r, g, b = [min(255, max(0, int(int(hex_color[j:j+2], 16) * jitter))) for j in (1, 3, 5)]
|
75 |
+
return f'rgba({r}, {g}, {b}, 0.9)'
|
76 |
+
final_colors.append({
|
77 |
+
'start': jitter_color(color['start']),
|
78 |
+
'end': jitter_color(color['end']).replace('0.9', '0.8')
|
79 |
+
})
|
80 |
+
return final_colors
|
81 |
+
|
82 |
+
# Knowledge map creator function (unchanged)
|
83 |
+
def create_knowledge_map(filtered_df, view_type='host'):
|
84 |
+
color_palette = {
|
85 |
+
'background': '#1E1E1E', # Dark background (almost black)
|
86 |
+
'card_bg': '#1A2238', # Bluish-black for cards (from your image)
|
87 |
+
'accent1': '#FF6A3D', # Orange for headings (keeping from original)
|
88 |
+
'accent2': '#4ECCA3', # Keeping teal for secondary elements
|
89 |
+
'accent3': '#9D84B7', # Keeping lavender for tertiary elements
|
90 |
+
'text_light': '#FFFFFF', # White text
|
91 |
+
'text_dark': '#E0E0E0', # Light grey text for dark backgrounds
|
92 |
+
}
|
93 |
+
|
94 |
+
if view_type == 'host':
|
95 |
+
group_col = 'host_organization_name'
|
96 |
+
id_col = 'host_organization_id'
|
97 |
+
title = "Host Organization Clusters"
|
98 |
+
else:
|
99 |
+
group_col = 'venue'
|
100 |
+
id_col = 'venue_id'
|
101 |
+
title = "Publication Venue Clusters"
|
102 |
+
summary = filtered_df.groupby(group_col).agg(
|
103 |
+
paper_count=('id', 'count'),
|
104 |
+
is_oa=('is_oa', 'mean'),
|
105 |
+
oa_status=('oa_status', lambda x: x.mode()[0] if not x.mode().empty else None),
|
106 |
+
entity_id=(id_col, 'first')
|
107 |
+
).reset_index()
|
108 |
+
paper_count_groups = defaultdict(list)
|
109 |
+
for _, row in summary.iterrows():
|
110 |
+
paper_count_groups[row['paper_count']].append(row)
|
111 |
+
knowledge_map_fig = go.Figure()
|
112 |
+
sorted_counts = sorted(paper_count_groups.keys(), reverse=True)
|
113 |
+
vibrant_colors = generate_vibrant_colors(len(sorted_counts))
|
114 |
+
golden_angle = np.pi * (3 - np.sqrt(5))
|
115 |
+
spiral_coef = 150
|
116 |
+
cluster_metadata = {}
|
117 |
+
max_x, max_y = 500, 500
|
118 |
+
for i, count in enumerate(sorted_counts):
|
119 |
+
radius = np.sqrt(i) * spiral_coef
|
120 |
+
theta = golden_angle * i
|
121 |
+
cluster_x, cluster_y = radius * np.cos(theta), radius * np.sin(theta)
|
122 |
+
label_offset_angle = theta + np.pi/4
|
123 |
+
label_offset_distance = 80 + 4 * np.sqrt(len(paper_count_groups[count]))
|
124 |
+
label_x = cluster_x + label_offset_distance * np.cos(label_offset_angle)
|
125 |
+
label_y = cluster_y + label_offset_distance * np.sin(label_offset_angle)
|
126 |
+
cluster_metadata[count] = {
|
127 |
+
'center_x': cluster_x,
|
128 |
+
'center_y': cluster_y,
|
129 |
+
'entities': paper_count_groups[count],
|
130 |
+
'color': vibrant_colors[i]
|
131 |
+
}
|
132 |
+
entities = paper_count_groups[count]
|
133 |
+
num_entities = len(entities)
|
134 |
+
cluster_size = min(200, max(80, 40 + 8 * np.sqrt(num_entities)))
|
135 |
+
color = vibrant_colors[i]
|
136 |
+
knowledge_map_fig.add_shape(
|
137 |
+
type="circle",
|
138 |
+
x0=cluster_x - cluster_size/2, y0=cluster_y - cluster_size/2,
|
139 |
+
x1=cluster_x + cluster_size/2, y1=cluster_y + cluster_size/2,
|
140 |
+
fillcolor=color['end'].replace("0.8", "0.15"),
|
141 |
+
line=dict(color=color['start'], width=1.5),
|
142 |
+
opacity=0.7
|
143 |
+
)
|
144 |
+
knowledge_map_fig.add_trace(go.Scatter(
|
145 |
+
x=[cluster_x], y=[cluster_y],
|
146 |
+
mode='markers',
|
147 |
+
marker=dict(size=cluster_size, color=color['start'], opacity=0.3),
|
148 |
+
customdata=[[count, "cluster"]],
|
149 |
+
hoverinfo='skip'
|
150 |
+
))
|
151 |
+
knowledge_map_fig.add_trace(go.Scatter(
|
152 |
+
x=[cluster_x, label_x], y=[cluster_y, label_y],
|
153 |
+
mode='lines',
|
154 |
+
line=dict(color=color['start'], width=1, dash='dot'),
|
155 |
+
hoverinfo='skip'
|
156 |
+
))
|
157 |
+
knowledge_map_fig.add_annotation(
|
158 |
+
x=label_x, y=label_y,
|
159 |
+
text=f"{count} papers<br>{num_entities} {'orgs' if view_type == 'host' else 'venues'}",
|
160 |
+
showarrow=False,
|
161 |
+
font=dict(size=11, color='white'),
|
162 |
+
bgcolor=color['start'],
|
163 |
+
bordercolor='white',
|
164 |
+
borderwidth=1,
|
165 |
+
opacity=0.9
|
166 |
+
)
|
167 |
+
entities_sorted = sorted(entities, key=lambda x: x[group_col])
|
168 |
+
inner_spiral_coef = 0.4
|
169 |
+
for j, entity_data in enumerate(entities_sorted):
|
170 |
+
spiral_radius = np.sqrt(j) * cluster_size * inner_spiral_coef / np.sqrt(num_entities + 1)
|
171 |
+
spiral_angle = golden_angle * j
|
172 |
+
jitter_radius = random.uniform(0.9, 1.1) * spiral_radius
|
173 |
+
jitter_angle = spiral_angle + random.uniform(-0.1, 0.1)
|
174 |
+
entity_x = cluster_x + jitter_radius * np.cos(jitter_angle)
|
175 |
+
entity_y = cluster_y + jitter_radius * np.sin(jitter_angle)
|
176 |
+
node_size = min(18, max(8, np.sqrt(entity_data['paper_count']) * 1.5))
|
177 |
+
knowledge_map_fig.add_trace(go.Scatter(
|
178 |
+
x=[entity_x], y=[entity_y],
|
179 |
+
mode='markers',
|
180 |
+
marker=dict(
|
181 |
+
size=node_size,
|
182 |
+
color=color['start'],
|
183 |
+
line=dict(color='rgba(255, 255, 255, 0.9)', width=1.5)
|
184 |
+
),
|
185 |
+
customdata=[[
|
186 |
+
entity_data[group_col],
|
187 |
+
entity_data['paper_count'],
|
188 |
+
entity_data['is_oa'],
|
189 |
+
entity_data['entity_id'],
|
190 |
+
count,
|
191 |
+
"entity"
|
192 |
+
]],
|
193 |
+
hovertemplate=(
|
194 |
+
f"<b>{entity_data[group_col]}</b><br>"
|
195 |
+
f"Papers: {entity_data['paper_count']}<br>"
|
196 |
+
f"Open Access: {entity_data['is_oa']:.1%}<extra></extra>"
|
197 |
+
)
|
198 |
+
))
|
199 |
+
max_x = max([abs(cluster['center_x']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
|
200 |
+
max_y = max([abs(cluster['center_y']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
|
201 |
+
# Update knowledge_map_fig layout
|
202 |
+
knowledge_map_fig.update_layout(
|
203 |
+
title=dict(
|
204 |
+
text=title,
|
205 |
+
font=dict(size=22, family='"Poppins", sans-serif', color=color_palette['accent1']) # Orange title
|
206 |
+
),
|
207 |
+
plot_bgcolor='rgba(26, 34, 56, 1)', # Bluish-black background
|
208 |
+
paper_bgcolor='rgba(26, 34, 56, 0.7)',
|
209 |
+
xaxis=dict(range=[-max(700, max_x), max(700, max_x)], showticklabels=False, showgrid=False),
|
210 |
+
yaxis=dict(range=[-max(500, max_y), max(500, max_y)], showticklabels=False, showgrid=False),
|
211 |
+
margin=dict(l=10, r=10, t=60, b=10),
|
212 |
+
height=700,
|
213 |
+
hovermode='closest',
|
214 |
+
showlegend=False,
|
215 |
+
font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']), # Light text
|
216 |
+
)
|
217 |
+
return knowledge_map_fig, cluster_metadata
|
218 |
+
|
219 |
+
# Other chart functions (unchanged)
|
220 |
+
def create_oa_pie_fig(filtered_df):
|
221 |
+
color_palette = {
|
222 |
+
'background': '#1A2238', # Dark blue background
|
223 |
+
'card_bg': '#1A2238', # Changed to match the other chart
|
224 |
+
'accent1': '#FF6A3D', # Vibrant orange for highlights
|
225 |
+
'accent2': '#4ECCA3', # Teal for secondary elements
|
226 |
+
'accent3': '#9D84B7', # Lavender for tertiary elements
|
227 |
+
'text_light': '#FFFFFF', # White text
|
228 |
+
'text_dark': '#FFFFFF', # Changed to white for better contrast
|
229 |
+
}
|
230 |
+
|
231 |
+
fig = px.pie(
|
232 |
+
filtered_df, names='is_oa', title="Overall Open Access Status",
|
233 |
+
labels={True: "Open Access", False: "Not Open Access"},
|
234 |
+
color_discrete_sequence=[color_palette['accent2'], color_palette['accent1']]
|
235 |
+
)
|
236 |
+
|
237 |
+
fig.update_traces(
|
238 |
+
textinfo='label+percent',
|
239 |
+
textfont=dict(size=14, family='"Poppins", sans-serif'),
|
240 |
+
marker=dict(line=dict(color='#1A2238', width=2)) # Match background color
|
241 |
+
)
|
242 |
+
|
243 |
+
fig.update_layout(
|
244 |
+
title=dict(
|
245 |
+
text="Overall Open Access Status",
|
246 |
+
font=dict(size=18, family='"Poppins", sans-serif', color=color_palette['accent1']) # Orange title
|
247 |
+
),
|
248 |
+
font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']),
|
249 |
+
paper_bgcolor=color_palette['background'], # Dark background
|
250 |
+
plot_bgcolor=color_palette['background'], # Dark background
|
251 |
+
margin=dict(t=50, b=20, l=20, r=20),
|
252 |
+
legend=dict(
|
253 |
+
orientation="h",
|
254 |
+
yanchor="bottom",
|
255 |
+
y=-0.2,
|
256 |
+
xanchor="center",
|
257 |
+
x=0.5,
|
258 |
+
font=dict(size=12, color=color_palette['text_light'])
|
259 |
+
)
|
260 |
+
)
|
261 |
+
|
262 |
+
return fig
|
263 |
+
def create_oa_status_pie_fig(filtered_df):
|
264 |
+
custom_colors = [
|
265 |
+
"#9D84B7",
|
266 |
+
'#4DADFF',
|
267 |
+
'#FFD166',
|
268 |
+
'#06D6A0',
|
269 |
+
'#EF476F'
|
270 |
+
]
|
271 |
+
fig = px.pie(
|
272 |
+
filtered_df,
|
273 |
+
names='oa_status',
|
274 |
+
title="Open Access Status Distribution",
|
275 |
+
color_discrete_sequence=custom_colors
|
276 |
+
)
|
277 |
+
fig.update_traces(
|
278 |
+
textinfo='label+percent',
|
279 |
+
insidetextorientation='radial',
|
280 |
+
textfont=dict(size=14, family='"Poppins", sans-serif'),
|
281 |
+
marker=dict(line=dict(color='#FFFFFF', width=2))
|
282 |
+
)
|
283 |
+
fig.update_layout(
|
284 |
+
title=dict(
|
285 |
+
text="Open Access Status Distribution",
|
286 |
+
font=dict(size=18, family='"Poppins", sans-serif', color="#FF6A3D")
|
287 |
+
),
|
288 |
+
font=dict(family='"Poppins", sans-serif', color='#FFFFFF'),
|
289 |
+
paper_bgcolor='#1A2238', # Bluish-black background
|
290 |
+
plot_bgcolor='#1A2238',
|
291 |
+
margin=dict(t=50, b=20, l=20, r=20),
|
292 |
+
legend=dict(
|
293 |
+
orientation="h",
|
294 |
+
yanchor="bottom",
|
295 |
+
y=-0.2,
|
296 |
+
xanchor="center",
|
297 |
+
x=0.5,
|
298 |
+
font=dict(size=12, color='#FFFFFF')
|
299 |
+
)
|
300 |
+
)
|
301 |
+
return fig
|
302 |
+
def create_type_bar_fig(filtered_df):
|
303 |
+
type_counts = filtered_df['type'].value_counts()
|
304 |
+
vibrant_colors = [
|
305 |
+
'#4361EE', '#3A0CA3', '#4CC9F0',
|
306 |
+
'#F72585', '#7209B7', '#B5179E',
|
307 |
+
'#480CA8', '#560BAD', '#F77F00'
|
308 |
+
]
|
309 |
+
fig = px.bar(
|
310 |
+
type_counts,
|
311 |
+
title="Publication Types",
|
312 |
+
labels={'value': 'Count', 'index': 'Type'},
|
313 |
+
color=type_counts.index,
|
314 |
+
color_discrete_sequence=vibrant_colors[:len(type_counts)]
|
315 |
+
)
|
316 |
+
fig.update_layout(
|
317 |
+
title=dict(
|
318 |
+
text="Publication Types",
|
319 |
+
font=dict(size=20, family='"Poppins", sans-serif', color="#FF6A3D") # Larger font size
|
320 |
+
),
|
321 |
+
xaxis_title="Type",
|
322 |
+
yaxis_title="Count",
|
323 |
+
font=dict(family='"Poppins", sans-serif', color="#FFFFFF", size=14), # Increased font size
|
324 |
+
paper_bgcolor='#1A2238', # Consistent dark background
|
325 |
+
plot_bgcolor='#1A2238', # Consistent dark background
|
326 |
+
margin=dict(t=70, b=60, l=60, r=40), # Increased margins
|
327 |
+
xaxis=dict(
|
328 |
+
tickfont=dict(size=14, color="#FFFFFF"), # Increased tick font size
|
329 |
+
tickangle=-45,
|
330 |
+
gridcolor='rgba(255, 255, 255, 0.1)' # Lighter grid lines
|
331 |
+
),
|
332 |
+
yaxis=dict(
|
333 |
+
tickfont=dict(size=14, color="#FFFFFF"), # Increased tick font size
|
334 |
+
gridcolor='rgba(255, 255, 255, 0.1)' # Lighter grid lines
|
335 |
+
),
|
336 |
+
bargap=0.3, # Increased bar gap
|
337 |
+
)
|
338 |
+
fig.update_traces(
|
339 |
+
marker_line_width=1,
|
340 |
+
marker_line_color='rgba(0, 0, 0, 0.5)',
|
341 |
+
opacity=0.9,
|
342 |
+
hovertemplate='%{y} publications<extra></extra>',
|
343 |
+
texttemplate='%{y}', # Add text labels
|
344 |
+
textposition='outside', # Position labels outside bars
|
345 |
+
textfont=dict(size=14, color='white') # Text label formatting
|
346 |
+
)
|
347 |
+
return fig
|
348 |
+
|
349 |
+
# Function to check if port is in use
|
350 |
+
def is_port_in_use(port):
|
351 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
352 |
+
return s.connect_ex(('localhost', port)) == 0
|
353 |
+
|
354 |
+
# Function to find a free port
|
355 |
+
def find_free_port(start_port=8050):
|
356 |
+
port = start_port
|
357 |
+
while is_port_in_use(port):
|
358 |
+
port += 1
|
359 |
+
return port
|
360 |
+
|
361 |
+
# Function to shutdown any existing dashboard
|
362 |
+
def shutdown_existing_dashboard():
|
363 |
+
global dashboard_process
|
364 |
+
|
365 |
+
# First, check if our port is in use
|
366 |
+
if is_port_in_use(dashboard_port):
|
367 |
+
try:
|
368 |
+
# Kill processes using the port
|
369 |
+
for proc in psutil.process_iter(['pid', 'name', 'connections']):
|
370 |
+
try:
|
371 |
+
for conn in proc.connections():
|
372 |
+
if conn.laddr.port == dashboard_port:
|
373 |
+
print(f"Terminating process {proc.pid} using port {dashboard_port}")
|
374 |
+
proc.terminate()
|
375 |
+
proc.wait(timeout=3)
|
376 |
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
377 |
+
pass
|
378 |
+
except Exception as e:
|
379 |
+
print(f"Error freeing port {dashboard_port}: {e}")
|
380 |
+
|
381 |
+
# If we're tracking a dashboard process, try to terminate it
|
382 |
+
if dashboard_process is not None:
|
383 |
+
try:
|
384 |
+
# Kill the process if it's still running
|
385 |
+
if dashboard_process.is_alive():
|
386 |
+
parent = psutil.Process(os.getpid())
|
387 |
+
children = parent.children(recursive=True)
|
388 |
+
for process in children:
|
389 |
+
try:
|
390 |
+
process.terminate()
|
391 |
+
except:
|
392 |
+
pass
|
393 |
+
dashboard_process = None
|
394 |
+
except Exception as e:
|
395 |
+
print(f"Error terminating dashboard process: {e}")
|
396 |
+
dashboard_process = None # Reset the reference anyway
|
397 |
+
|
398 |
+
# Pydantic model for request validation
|
399 |
+
class DashboardRequest(BaseModel):
|
400 |
+
userId: str
|
401 |
+
topic: str
|
402 |
+
year: int
|
403 |
+
|
404 |
+
@router.post("/load_and_display_dashboard/")
|
405 |
+
async def load_and_display_dashboard(request: DashboardRequest, req:Request):
|
406 |
+
global dashboard_process, dashboard_port
|
407 |
+
|
408 |
+
# Make sure any existing dashboard is shut down
|
409 |
+
shutdown_existing_dashboard()
|
410 |
+
|
411 |
+
# Find a free port
|
412 |
+
dashboard_port = find_free_port()
|
413 |
+
|
414 |
+
try:
|
415 |
+
# Load data from MongoDB
|
416 |
+
df = await load_data_from_mongodb(request.userId, request.topic, request.year, req)
|
417 |
+
|
418 |
+
# Get date range for the slider
|
419 |
+
global min_date, max_date, date_range, date_marks
|
420 |
+
min_date = df['publication_date'].min()
|
421 |
+
max_date = df['publication_date'].max()
|
422 |
+
date_range = pd.date_range(start=min_date, end=max_date, freq='MS')
|
423 |
+
date_marks = {i: date.strftime('%b %Y') for i, date in enumerate(date_range)}
|
424 |
+
|
425 |
+
# Function to create and run the dashboard
|
426 |
+
def create_and_run_dashboard():
|
427 |
+
# Create a new app instance
|
428 |
+
app = Dash(__name__, suppress_callback_exceptions=True)
|
429 |
+
app.cluster_metadata = {}
|
430 |
+
color_palette = {
|
431 |
+
'background': '#1A2238', # Dark blue background
|
432 |
+
'card_bg': '#F8F8FF', # Off-white for cards
|
433 |
+
'accent1': '#FF6A3D', # Vibrant orange for highlights
|
434 |
+
'accent2': '#4ECCA3', # Teal for secondary elements
|
435 |
+
'accent3': '#9D84B7', # Lavender for tertiary elements
|
436 |
+
'text_light': '#FFFFFF', # White text
|
437 |
+
'text_dark': '#2D3748', # Dark gray text
|
438 |
+
}
|
439 |
+
|
440 |
+
# Define modern styling for containers
|
441 |
+
container_style = {
|
442 |
+
'padding': '5px',
|
443 |
+
'backgroundColor': color_palette['text_dark'],
|
444 |
+
'borderRadius': '12px',
|
445 |
+
'boxShadow': '0 4px 12px rgba(0, 0, 0, 0.15)',
|
446 |
+
'marginBottom': '25px',
|
447 |
+
'border': f'1px solid rgba(255, 255, 255, 0.2)',
|
448 |
+
|
449 |
+
}
|
450 |
+
|
451 |
+
hidden_style = {**container_style, 'display': 'none'}
|
452 |
+
visible_style = {**container_style}
|
453 |
+
|
454 |
+
# Create a modern, attractive layout
|
455 |
+
app.layout = html.Div([
|
456 |
+
# Header section with gradient background
|
457 |
+
html.Div([
|
458 |
+
html.H1(request.topic.capitalize() + " Analytics Dashboard", style={
|
459 |
+
'textAlign': 'center',
|
460 |
+
'marginBottom': '10px',
|
461 |
+
'color': color_palette['accent1'],
|
462 |
+
'fontSize': '2.5rem',
|
463 |
+
'fontWeight': '700',
|
464 |
+
'letterSpacing': '0.5px',
|
465 |
+
}),
|
466 |
+
html.Div([
|
467 |
+
html.P("Research Publication Analysis & Knowledge Mapping", style={
|
468 |
+
'textAlign': 'center',
|
469 |
+
'color': color_palette['text_light'],
|
470 |
+
'opacity': '0.8',
|
471 |
+
'fontSize': '1.2rem',
|
472 |
+
'marginTop': '0',
|
473 |
+
})
|
474 |
+
])
|
475 |
+
], style={
|
476 |
+
'background': f'linear-gradient(135deg, {color_palette["background"]}, #364156)',
|
477 |
+
'padding': '30px 20px',
|
478 |
+
'borderRadius': '12px',
|
479 |
+
'marginBottom': '25px',
|
480 |
+
'boxShadow': '0 4px 20px rgba(0, 0, 0, 0.2)',
|
481 |
+
}),
|
482 |
+
|
483 |
+
# Controls section
|
484 |
+
html.Div([
|
485 |
+
html.Div([
|
486 |
+
html.Button(
|
487 |
+
id='view-toggle',
|
488 |
+
children='Switch to Venue View',
|
489 |
+
style={
|
490 |
+
'padding': '12px 20px',
|
491 |
+
'fontSize': '1rem',
|
492 |
+
'borderRadius': '8px',
|
493 |
+
'border': 'none',
|
494 |
+
'backgroundColor': color_palette['accent1'],
|
495 |
+
'color': 'white',
|
496 |
+
'cursor': 'pointer',
|
497 |
+
'boxShadow': '0 2px 5px rgba(0, 0, 0, 0.1)',
|
498 |
+
'transition': 'all 0.3s ease',
|
499 |
+
'marginRight': '20px',
|
500 |
+
'fontWeight': '500',
|
501 |
+
}
|
502 |
+
),
|
503 |
+
html.H3("Filter by Publication Date", style={
|
504 |
+
'marginBottom': '15px',
|
505 |
+
'color': color_palette['text_dark'],
|
506 |
+
'fontSize': '1.3rem',
|
507 |
+
'fontWeight': '600',
|
508 |
+
}),
|
509 |
+
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '15px'}),
|
510 |
+
|
511 |
+
dcc.RangeSlider(
|
512 |
+
id='date-slider',
|
513 |
+
min=0,
|
514 |
+
max=len(date_range) - 1,
|
515 |
+
value=[0, len(date_range) - 1],
|
516 |
+
marks=date_marks if len(date_marks) <= 12 else {
|
517 |
+
i: date_marks[i] for i in range(0, len(date_range), max(1, len(date_range) // 12))
|
518 |
+
},
|
519 |
+
step=1,
|
520 |
+
tooltip={"placement": "bottom", "always_visible": True},
|
521 |
+
updatemode='mouseup'
|
522 |
+
),
|
523 |
+
html.Div(id='date-range-display', style={
|
524 |
+
'textAlign': 'center',
|
525 |
+
'marginTop': '12px',
|
526 |
+
'fontSize': '1.1rem',
|
527 |
+
'fontWeight': '500',
|
528 |
+
'color': color_palette['accent1'],
|
529 |
+
})
|
530 |
+
], style={**container_style, 'marginBottom': '25px'}),
|
531 |
+
|
532 |
+
# Knowledge map - main visualization
|
533 |
+
html.Div([
|
534 |
+
dcc.Graph(
|
535 |
+
id='knowledge-map',
|
536 |
+
style={'width': '100%', 'height': '700px'},
|
537 |
+
config={'scrollZoom': True, 'displayModeBar': True, 'responsive': True}
|
538 |
+
)
|
539 |
+
], style={
|
540 |
+
**container_style,
|
541 |
+
'height': '750px',
|
542 |
+
'marginBottom': '25px',
|
543 |
+
'background': f'linear-gradient(to bottom right, {color_palette["card_bg"]}, #F0F0F8)',
|
544 |
+
}),
|
545 |
+
|
546 |
+
# Details container - appears when clicking elements
|
547 |
+
html.Div([
|
548 |
+
html.H3(id='details-title', style={
|
549 |
+
'marginBottom': '15px',
|
550 |
+
'color': color_palette['accent1'],
|
551 |
+
'fontSize': '1.4rem',
|
552 |
+
'fontWeight': '600',
|
553 |
+
}),
|
554 |
+
html.Div(id='details-content', style={
|
555 |
+
'maxHeight': '350px',
|
556 |
+
'overflowY': 'auto',
|
557 |
+
'padding': '10px',
|
558 |
+
'borderRadius': '8px',
|
559 |
+
'backgroundColor': 'rgba(255, 255, 255, 0.7)',
|
560 |
+
})
|
561 |
+
], id='details-container', style=hidden_style),
|
562 |
+
|
563 |
+
# Charts in flex container
|
564 |
+
html.Div([
|
565 |
+
html.Div([
|
566 |
+
dcc.Graph(
|
567 |
+
id='oa-pie-chart',
|
568 |
+
style={'width': '100%', 'height': '350px'},
|
569 |
+
config={'displayModeBar': False, 'responsive': True}
|
570 |
+
)
|
571 |
+
], style={
|
572 |
+
'flex': 1,
|
573 |
+
**container_style,
|
574 |
+
'margin': '0 10px',
|
575 |
+
'height': '400px',
|
576 |
+
'transition': 'transform 0.3s ease',
|
577 |
+
':hover': {'transform': 'translateY(-5px)'},
|
578 |
+
}),
|
579 |
+
html.Div([
|
580 |
+
dcc.Graph(
|
581 |
+
id='oa-status-pie-chart',
|
582 |
+
style={'width': '100%', 'height': '350px'},
|
583 |
+
config={'displayModeBar': False, 'responsive': True}
|
584 |
+
)
|
585 |
+
], style={
|
586 |
+
'flex': 1,
|
587 |
+
**container_style,
|
588 |
+
'margin': '0 10px',
|
589 |
+
'height': '400px',
|
590 |
+
'transition': 'transform 0.3s ease',
|
591 |
+
':hover': {'transform': 'translateY(-5px)'},
|
592 |
+
})
|
593 |
+
], style={'display': 'flex', 'marginBottom': '25px', 'height': '420px'}),
|
594 |
+
|
595 |
+
# Bar chart container
|
596 |
+
# Increase bar chart height and improve visibility
|
597 |
+
html.Div([
|
598 |
+
dcc.Graph(
|
599 |
+
id='type-bar-chart',
|
600 |
+
style={'width': '100%', 'height': '50vh'}, # Reduced from 60vh
|
601 |
+
config={'displayModeBar': False, 'responsive': True}
|
602 |
+
)
|
603 |
+
], style={
|
604 |
+
**container_style,
|
605 |
+
'height': '500px', # Decreased from 650px
|
606 |
+
'background': 'rgba(26, 34, 56, 1)',
|
607 |
+
'marginBottom': '10px', # Added smaller bottom margin
|
608 |
+
}),
|
609 |
+
# Store components for state
|
610 |
+
dcc.Store(id='filtered-df-info'),
|
611 |
+
dcc.Store(id='current-view', data='host'),
|
612 |
+
html.Div(id='load-trigger', children='trigger-initial-load', style={'display': 'none'})
|
613 |
+
], style={
|
614 |
+
'fontFamily': '"Poppins", "Segoe UI", Arial, sans-serif',
|
615 |
+
'backgroundColor': '#121212', # Dark background
|
616 |
+
'backgroundImage': 'none', # Remove gradient
|
617 |
+
'padding': '30px',
|
618 |
+
'maxWidth': '1800px',
|
619 |
+
'margin': '0 auto',
|
620 |
+
'minHeight': '100vh',
|
621 |
+
'color': color_palette['text_light'],
|
622 |
+
'paddingBottom': '10px',
|
623 |
+
})
|
624 |
+
|
625 |
+
|
626 |
+
|
627 |
+
@app.callback(
|
628 |
+
[Output('current-view', 'data'),
|
629 |
+
Output('view-toggle', 'children')],
|
630 |
+
[Input('view-toggle', 'n_clicks')],
|
631 |
+
[State('current-view', 'data')]
|
632 |
+
)
|
633 |
+
def toggle_view(n_clicks, current_view):
|
634 |
+
if not n_clicks:
|
635 |
+
return current_view, 'Switch to Venue View' if current_view == 'host' else 'Switch to Host View'
|
636 |
+
new_view = 'venue' if current_view == 'host' else 'host'
|
637 |
+
new_button_text = 'Switch to Host View' if new_view == 'venue' else 'Switch to Venue View'
|
638 |
+
return new_view, new_button_text
|
639 |
+
|
640 |
+
@app.callback(
|
641 |
+
Output('date-range-display', 'children'),
|
642 |
+
[Input('date-slider', 'value')]
|
643 |
+
)
|
644 |
+
def update_date_range_display(date_range_indices):
|
645 |
+
start_date = date_range[date_range_indices[0]]
|
646 |
+
end_date = date_range[date_range_indices[1]]
|
647 |
+
return f"Selected period: {start_date.strftime('%b %Y')} to {end_date.strftime('%b %Y')}"
|
648 |
+
|
649 |
+
@app.callback(
|
650 |
+
[Output('knowledge-map', 'figure'),
|
651 |
+
Output('oa-pie-chart', 'figure'),
|
652 |
+
Output('oa-status-pie-chart', 'figure'),
|
653 |
+
Output('type-bar-chart', 'figure'),
|
654 |
+
Output('filtered-df-info', 'data'),
|
655 |
+
Output('details-container', 'style')],
|
656 |
+
[Input('date-slider', 'value'),
|
657 |
+
Input('current-view', 'data'),
|
658 |
+
Input('load-trigger', 'children')] # Added trigger
|
659 |
+
)
|
660 |
+
def update_visualizations(date_range_indices, current_view, _):
|
661 |
+
filtered_df = filter_by_date_range(df, date_range_indices[0], date_range_indices[1])
|
662 |
+
knowledge_map_fig, cluster_metadata = create_knowledge_map(filtered_df, current_view)
|
663 |
+
app.cluster_metadata = cluster_metadata
|
664 |
+
filtered_info = {
|
665 |
+
'start_idx': date_range_indices[0],
|
666 |
+
'end_idx': date_range_indices[1],
|
667 |
+
'start_date': date_range[date_range_indices[0]].strftime('%Y-%m-%d'),
|
668 |
+
'end_date': date_range[date_range_indices[1]].strftime('%Y-%m-%d'),
|
669 |
+
'record_count': len(filtered_df),
|
670 |
+
'view_type': current_view
|
671 |
+
}
|
672 |
+
return (
|
673 |
+
knowledge_map_fig,
|
674 |
+
create_oa_pie_fig(filtered_df),
|
675 |
+
create_oa_status_pie_fig(filtered_df),
|
676 |
+
create_type_bar_fig(filtered_df),
|
677 |
+
filtered_info,
|
678 |
+
hidden_style
|
679 |
+
)
|
680 |
+
|
681 |
+
@app.callback(
|
682 |
+
[Output('details-container', 'style', allow_duplicate=True),
|
683 |
+
Output('details-title', 'children'),
|
684 |
+
Output('details-content', 'children')],
|
685 |
+
[Input('knowledge-map', 'clickData')],
|
686 |
+
[State('filtered-df-info', 'data')],
|
687 |
+
prevent_initial_call=True
|
688 |
+
)
|
689 |
+
def display_details(clickData, filtered_info):
|
690 |
+
if not clickData or not filtered_info:
|
691 |
+
return hidden_style, "", []
|
692 |
+
customdata = clickData['points'][0]['customdata']
|
693 |
+
view_type = filtered_info['view_type']
|
694 |
+
entity_type = "Organization" if view_type == 'host' else "Venue"
|
695 |
+
if len(customdata) >= 2 and customdata[-1] == "cluster":
|
696 |
+
count = customdata[0]
|
697 |
+
if count not in app.cluster_metadata:
|
698 |
+
return hidden_style, "", []
|
699 |
+
entities = app.cluster_metadata[count]['entities']
|
700 |
+
color = app.cluster_metadata[count]['color']['start']
|
701 |
+
table_header = [
|
702 |
+
html.Thead(html.Tr([
|
703 |
+
html.Th(f"{entity_type} Name", style={'padding': '8px'}),
|
704 |
+
html.Th(f"{entity_type} ID", style={'padding': '8px'}),
|
705 |
+
html.Th("Papers", style={'padding': '8px', 'textAlign': 'center'}),
|
706 |
+
html.Th("Open Access %", style={'padding': '8px', 'textAlign': 'center'})
|
707 |
+
], style={'backgroundColor': color_palette['accent1'], 'color': 'white'}))
|
708 |
+
]
|
709 |
+
|
710 |
+
# Update row styles
|
711 |
+
row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
|
712 |
+
rows = []
|
713 |
+
for i, entity in enumerate(sorted(entities, key=lambda x: x['paper_count'], reverse=True)):
|
714 |
+
row_style = {'backgroundColor': '#f9f9f9'} if i % 2 == 0 else {'backgroundColor': 'white'}
|
715 |
+
entity_name_link = html.A(
|
716 |
+
entity[f"{view_type}_organization_name" if view_type == 'host' else "venue"],
|
717 |
+
href=entity['entity_id'],
|
718 |
+
target="_blank",
|
719 |
+
style={'color': color, 'textDecoration': 'underline'}
|
720 |
+
)
|
721 |
+
entity_id_link = html.A(
|
722 |
+
entity['entity_id'].split('/')[-1],
|
723 |
+
href=entity['entity_id'],
|
724 |
+
target="_blank",
|
725 |
+
style={'color': color, 'textDecoration': 'underline'}
|
726 |
+
)
|
727 |
+
rows.append(html.Tr([
|
728 |
+
html.Td(entity_name_link, style={'padding': '8px'}),
|
729 |
+
html.Td(entity_id_link, style={'padding': '8px'}),
|
730 |
+
html.Td(entity['paper_count'], style={'padding': '8px', 'textAlign': 'center'}),
|
731 |
+
html.Td(f"{entity['is_oa']:.1%}", style={'padding': '8px', 'textAlign': 'center'})
|
732 |
+
], style=row_style))
|
733 |
+
table = html.Table(table_header + [html.Tbody(rows)], style={
|
734 |
+
'width': '100%',
|
735 |
+
'borderCollapse': 'collapse',
|
736 |
+
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
|
737 |
+
})
|
738 |
+
return (
|
739 |
+
visible_style,
|
740 |
+
f"{entity_type}s with {count} papers",
|
741 |
+
[html.P(f"Showing {len(entities)} {entity_type.lower()}s during selected period"), table]
|
742 |
+
)
|
743 |
+
elif len(customdata) >= 6 and customdata[-1] == "entity":
|
744 |
+
entity_name = customdata[0]
|
745 |
+
entity_id = customdata[3]
|
746 |
+
cluster_count = customdata[4]
|
747 |
+
color = app.cluster_metadata[cluster_count]['color']['start']
|
748 |
+
if view_type == 'host':
|
749 |
+
entity_papers = df[df['host_organization_name'] == entity_name].copy()
|
750 |
+
else:
|
751 |
+
entity_papers = df[df['venue'] == entity_name].copy()
|
752 |
+
entity_papers = entity_papers[
|
753 |
+
(entity_papers['publication_date'] >= pd.to_datetime(filtered_info['start_date'])) &
|
754 |
+
(entity_papers['publication_date'] <= pd.to_datetime(filtered_info['end_date']))
|
755 |
+
]
|
756 |
+
entity_name_link = html.A(
|
757 |
+
entity_name,
|
758 |
+
href=entity_id,
|
759 |
+
target="_blank",
|
760 |
+
style={'color': color, 'textDecoration': 'underline', 'fontSize': '1.2em'}
|
761 |
+
)
|
762 |
+
entity_id_link = html.A(
|
763 |
+
entity_id.split('/')[-1],
|
764 |
+
href=entity_id,
|
765 |
+
target="_blank",
|
766 |
+
style={'color': color, 'textDecoration': 'underline'}
|
767 |
+
)
|
768 |
+
header = [
|
769 |
+
html.Div([
|
770 |
+
html.Span("Name: ", style={'fontWeight': 'bold'}),
|
771 |
+
entity_name_link
|
772 |
+
], style={'marginBottom': '10px'}),
|
773 |
+
html.Div([
|
774 |
+
html.Span("ID: ", style={'fontWeight': 'bold'}),
|
775 |
+
entity_id_link
|
776 |
+
], style={'marginBottom': '10px'}),
|
777 |
+
html.Div([
|
778 |
+
html.Span(f"Papers: {len(entity_papers)}", style={'marginRight': '20px'}),
|
779 |
+
], style={'marginBottom': '20px'})
|
780 |
+
]
|
781 |
+
table_header = [
|
782 |
+
html.Thead(html.Tr([
|
783 |
+
html.Th("Paper ID", style={'padding': '8px'}),
|
784 |
+
html.Th("Type", style={'padding': '8px'}),
|
785 |
+
html.Th("OA Status", style={'padding': '8px', 'textAlign': 'center'}),
|
786 |
+
html.Th("Publication Date", style={'padding': '8px', 'textAlign': 'center'})
|
787 |
+
], style={'backgroundColor': color, 'color': 'white'}))
|
788 |
+
]
|
789 |
+
rows = []
|
790 |
+
for i, (_, paper) in enumerate(entity_papers.sort_values('publication_date', ascending=False).iterrows()):
|
791 |
+
row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
|
792 |
+
paper_link = html.A(
|
793 |
+
paper['id'],
|
794 |
+
href=paper['id'],
|
795 |
+
target="_blank",
|
796 |
+
style={'color': color, 'textDecoration': 'underline'}
|
797 |
+
)
|
798 |
+
rows.append(html.Tr([
|
799 |
+
html.Td(paper_link, style={'padding': '8px'}),
|
800 |
+
html.Td(paper['type'], style={'padding': '8px'}),
|
801 |
+
html.Td(paper['oa_status'], style={'padding': '8px', 'textAlign': 'center'}),
|
802 |
+
html.Td(paper['publication_date'].strftime('%Y-%m-%d'), style={'padding': '8px', 'textAlign': 'center'})
|
803 |
+
], style=row_style))
|
804 |
+
table = html.Table(table_header + [html.Tbody(rows)], style={
|
805 |
+
'width': '100%',
|
806 |
+
'borderCollapse': 'collapse',
|
807 |
+
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
|
808 |
+
})
|
809 |
+
with open("dashboard.html", "w") as f:
|
810 |
+
f.write(app.index())
|
811 |
+
print("yup saved!!")
|
812 |
+
return visible_style, f"{entity_type} Papers", header + [table]
|
813 |
+
return hidden_style, "", []
|
814 |
+
|
815 |
+
# Start the Dash app
|
816 |
+
app.run_server(debug=False, port=dashboard_port, use_reloader=False)
|
817 |
+
|
818 |
+
# Run the dashboard in a separate process
|
819 |
+
dashboard_process = threading.Thread(target=create_and_run_dashboard)
|
820 |
+
dashboard_process.daemon = True
|
821 |
+
dashboard_process.start()
|
822 |
+
|
823 |
+
# Open the browser after a delay
|
824 |
+
def open_browser():
|
825 |
+
try:
|
826 |
+
webbrowser.open_new(f"http://127.0.0.1:{dashboard_port}/")
|
827 |
+
except:
|
828 |
+
pass
|
829 |
+
|
830 |
+
threading.Timer(1.5, open_browser).start()
|
831 |
+
|
832 |
+
return {"status": "success", "message": f"Dashboard loaded successfully on port {dashboard_port}."}
|
833 |
+
|
834 |
+
except Exception as e:
|
835 |
+
# Clean up in case of failure
|
836 |
+
shutdown_existing_dashboard()
|
837 |
+
raise HTTPException(status_code=400, detail=str(e))
|
venuedata.py
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import HTTPException, APIRouter, Request
|
2 |
+
from pydantic import BaseModel
|
3 |
+
import requests
|
4 |
+
from time import sleep
|
5 |
+
import spacy
|
6 |
+
from pymongo import MongoClient
|
7 |
+
|
8 |
+
# Load SciSpacy model
|
9 |
+
nlp = spacy.load("en_core_sci_sm")
|
10 |
+
|
11 |
+
router = APIRouter()
|
12 |
+
|
13 |
+
# Pydantic model for request body
|
14 |
+
class SearchRequest(BaseModel):
|
15 |
+
topic: str
|
16 |
+
year: int
|
17 |
+
userId: str # Add userId to the request body
|
18 |
+
|
19 |
+
|
20 |
+
|
21 |
+
# Function to extract keywords from user topic
|
22 |
+
def extract_keywords(text):
|
23 |
+
doc = nlp(text.lower())
|
24 |
+
noun_chunks = [chunk.text.strip() for chunk in doc.noun_chunks]
|
25 |
+
individual_tokens = [
|
26 |
+
token.text.strip() for token in doc
|
27 |
+
if token.pos_ in ["NOUN", "VERB"] and not token.is_stop
|
28 |
+
]
|
29 |
+
keywords = set(noun_chunks + individual_tokens)
|
30 |
+
|
31 |
+
cleaned_keywords = set()
|
32 |
+
for keyword in keywords:
|
33 |
+
if not any(keyword in chunk and keyword != chunk for chunk in noun_chunks):
|
34 |
+
cleaned_keywords.add(keyword)
|
35 |
+
return sorted(list(cleaned_keywords))
|
36 |
+
|
37 |
+
# Fetch works from OpenAlex based on refined topic
|
38 |
+
def fetch_works(refined_query, year, per_page=200):
|
39 |
+
OPENALEX_API_URL = f"https://api.openalex.org/works"
|
40 |
+
params = {
|
41 |
+
"filter": f"title_and_abstract.search:{refined_query},publication_year:{year}",
|
42 |
+
"per_page": per_page,
|
43 |
+
"cursor": "*"
|
44 |
+
}
|
45 |
+
all_results = []
|
46 |
+
|
47 |
+
while True:
|
48 |
+
response = requests.get(OPENALEX_API_URL, params=params)
|
49 |
+
if response.status_code != 200:
|
50 |
+
raise HTTPException(status_code=response.status_code, detail="Error fetching data from OpenAlex")
|
51 |
+
|
52 |
+
data = response.json()
|
53 |
+
all_results.extend(data.get("results", []))
|
54 |
+
|
55 |
+
next_cursor = data.get("meta", {}).get("next_cursor")
|
56 |
+
if next_cursor:
|
57 |
+
params["cursor"] = next_cursor
|
58 |
+
else:
|
59 |
+
break
|
60 |
+
|
61 |
+
sleep(0.2)
|
62 |
+
|
63 |
+
return all_results
|
64 |
+
|
65 |
+
# Batch fetch host organization details for sources
|
66 |
+
def batch_fetch_host_org_details(source_ids):
|
67 |
+
host_org_map = {}
|
68 |
+
batch_size = 100
|
69 |
+
for i in range(0, len(source_ids), batch_size):
|
70 |
+
batch = source_ids[i:i + batch_size]
|
71 |
+
openalex_ids = [src_id.split("/")[-1] for src_id in batch]
|
72 |
+
filter_query = "|".join(openalex_ids)
|
73 |
+
url = f"https://api.openalex.org/sources?filter=openalex_id:{filter_query}"
|
74 |
+
response = requests.get(url)
|
75 |
+
if response.status_code == 200:
|
76 |
+
sources = response.json().get("results", [])
|
77 |
+
for source in sources:
|
78 |
+
source_id = source.get("id")
|
79 |
+
# Handle the case where host_organization is a string (URL)
|
80 |
+
host_org_id = source.get("host_organization", "unknown")
|
81 |
+
host_org_name = source.get("host_organization_name", "unknown")
|
82 |
+
|
83 |
+
host_org_map[source_id] = {
|
84 |
+
"host_organization_name": host_org_name,
|
85 |
+
"host_organization_id": host_org_id
|
86 |
+
}
|
87 |
+
sleep(0.1)
|
88 |
+
else:
|
89 |
+
raise HTTPException(status_code=response.status_code, detail="Error fetching host organization details")
|
90 |
+
return host_org_map
|
91 |
+
|
92 |
+
# Extract metadata from works
|
93 |
+
def extract_metadata(works):
|
94 |
+
source_ids = list({
|
95 |
+
work["primary_location"]["source"]["id"]
|
96 |
+
for work in works
|
97 |
+
if work.get("primary_location") and work["primary_location"].get("source")
|
98 |
+
})
|
99 |
+
|
100 |
+
# Fetch host organization details (name and ID)
|
101 |
+
host_org_map = batch_fetch_host_org_details(source_ids)
|
102 |
+
|
103 |
+
metadata = []
|
104 |
+
for work in works:
|
105 |
+
primary_location = work.get("primary_location", {}) or {}
|
106 |
+
source = primary_location.get("source", {}) or {}
|
107 |
+
|
108 |
+
source_id = source.get("id")
|
109 |
+
host_org_details = host_org_map.get(source_id, {"host_organization_name": "Unknown", "host_organization_id": "Unknown"})
|
110 |
+
|
111 |
+
# Extract type field
|
112 |
+
work_type = (
|
113 |
+
work.get("type") or # Check the top-level "type" field
|
114 |
+
primary_location.get("type") or # Check "type" in primary_location
|
115 |
+
source.get("type") or # Check "type" in source
|
116 |
+
"unknown" # Fallback value
|
117 |
+
)
|
118 |
+
|
119 |
+
metadata.append({
|
120 |
+
"id": work.get("id"),
|
121 |
+
"is_oa": work.get("open_access", {}).get("is_oa", False),
|
122 |
+
"oa_status": work.get("open_access", {}).get("oa_status", "unknown"),
|
123 |
+
"venue": source.get("display_name", "unknown"),
|
124 |
+
"venue_id": source_id or "unknown",
|
125 |
+
"host_organization_name": host_org_details["host_organization_name"],
|
126 |
+
"host_organization_id": host_org_details["host_organization_id"],
|
127 |
+
"type": work_type,
|
128 |
+
"publication_date": work.get("publication_date", "unknown"),
|
129 |
+
})
|
130 |
+
return metadata
|
131 |
+
|
132 |
+
# Clean metadata by removing entries with unknown venue and host organization name
|
133 |
+
def clean_metadata(metadata):
|
134 |
+
initial_count = len(metadata)
|
135 |
+
cleaned_metadata = [
|
136 |
+
entry for entry in metadata
|
137 |
+
if not (entry["venue"] == "unknown" and entry["host_organization_name"] == "Unknown")
|
138 |
+
]
|
139 |
+
final_count = len(cleaned_metadata)
|
140 |
+
print(f"Total papers before cleaning: {initial_count}")
|
141 |
+
print(f"Total papers after cleaning: {final_count}")
|
142 |
+
return cleaned_metadata
|
143 |
+
|
144 |
+
# Save metadata to MongoDB
|
145 |
+
async def save_to_mongodb(userId, topic, year, metadata,request:Request):
|
146 |
+
document = {
|
147 |
+
"userId": userId,
|
148 |
+
"topic": topic,
|
149 |
+
"year": year,
|
150 |
+
"metadata": metadata
|
151 |
+
}
|
152 |
+
collection = request.app.state.collection2
|
153 |
+
await collection.update_one(
|
154 |
+
{"userId": userId, "topic": topic, "year": year},
|
155 |
+
{"$set": document},
|
156 |
+
upsert=True
|
157 |
+
)
|
158 |
+
print(f"Data saved to MongoDB for userId: {userId}, topic: {topic}, year: {year}")
|
159 |
+
|
160 |
+
# FastAPI endpoint
|
161 |
+
@router.post("/search/")
|
162 |
+
async def search(data:SearchRequest,request: Request):
|
163 |
+
userId = data.userId
|
164 |
+
topic = data.topic
|
165 |
+
year = data.year
|
166 |
+
|
167 |
+
# Extract keywords and refine query
|
168 |
+
keywords = extract_keywords(topic)
|
169 |
+
refined_query = "+".join(keywords)
|
170 |
+
print(f"Using refined search query: {refined_query}")
|
171 |
+
|
172 |
+
# Fetch works from OpenAlex
|
173 |
+
works = fetch_works(refined_query, year)
|
174 |
+
if not works:
|
175 |
+
raise HTTPException(status_code=404, detail="No works found for the given query")
|
176 |
+
|
177 |
+
# Extract metadata
|
178 |
+
metadata = extract_metadata(works)
|
179 |
+
|
180 |
+
# Clean metadata
|
181 |
+
cleaned_metadata = clean_metadata(metadata)
|
182 |
+
|
183 |
+
# Save metadata to MongoDB
|
184 |
+
await save_to_mongodb(userId, topic, year, cleaned_metadata,request)
|
185 |
+
|
186 |
+
# Return metadata as JSON response
|
187 |
+
return {"message": f"Data saved to MongoDB for userId: {userId}, topic: {topic}, year: {year}", "metadata": cleaned_metadata}
|
188 |
+
|
189 |
+
@router.post("/check-data-exists-venue/")
|
190 |
+
async def check_data_exists(request_data: SearchRequest, request:Request):
|
191 |
+
# Create a query to check if the data exists
|
192 |
+
query = {
|
193 |
+
"userId": request_data.userId,
|
194 |
+
"topic": request_data.topic
|
195 |
+
}
|
196 |
+
|
197 |
+
# Add year to query if it's provided
|
198 |
+
if request_data.year:
|
199 |
+
query["year"] = request_data.year
|
200 |
+
|
201 |
+
collection = request.app.state.collection2
|
202 |
+
# Check if a document matching the query exists
|
203 |
+
document = await collection.find_one(query) # Await the async operation
|
204 |
+
|
205 |
+
# Return result
|
206 |
+
return {
|
207 |
+
"exists": document is not None,
|
208 |
+
"message": "Data found" if document else "Data not found"
|
209 |
+
}
|
210 |
+
|
211 |
+
|