mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat(profiles): Add basic functionality for profile page
- Create ProfileCard and ProfileModal components - Implement profile.py backend file for CRUD operations - Update API file with profile-related functions - Modify main application file to include profile blueprint - Add profile directory initialization
This commit is contained in:
committed by
Sam Chau
parent
330c162b0e
commit
8cb3d7a827
@@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "run.py"]
|
||||
CMD ["python", "run.py"]
|
||||
@@ -3,25 +3,29 @@ from flask_cors import CORS
|
||||
from .regex import bp as regex_bp
|
||||
from .format import bp as format_bp
|
||||
from .settings import bp as settings_bp
|
||||
from .profile import bp as profile_bp
|
||||
import os
|
||||
|
||||
REGEX_DIR = os.path.join('data', 'db', 'regex_patterns')
|
||||
FORMAT_DIR = os.path.join('data', 'db', 'custom_formats')
|
||||
PROFILE_DIR = os.path.join('data', 'db', 'profiles')
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||||
|
||||
|
||||
# Initialize directories to avoid issues with non-existing directories
|
||||
initialize_directories()
|
||||
|
||||
|
||||
# Register Blueprints
|
||||
app.register_blueprint(regex_bp)
|
||||
app.register_blueprint(format_bp)
|
||||
app.register_blueprint(settings_bp)
|
||||
|
||||
app.register_blueprint(profile_bp)
|
||||
|
||||
return app
|
||||
|
||||
def initialize_directories():
|
||||
os.makedirs(REGEX_DIR, exist_ok=True)
|
||||
os.makedirs(FORMAT_DIR, exist_ok=True)
|
||||
os.makedirs(PROFILE_DIR, exist_ok=True)
|
||||
112
backend/app/profile.py
Normal file
112
backend/app/profile.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from collections import OrderedDict
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
from .utils import get_next_id, generate_filename, get_current_timestamp, sanitize_input
|
||||
|
||||
bp = Blueprint('profile', __name__, url_prefix='/profile')
|
||||
DATA_DIR = '/app/data'
|
||||
PROFILE_DIR = os.path.join(DATA_DIR, 'db', 'profiles')
|
||||
|
||||
# Ensure the directory exists
|
||||
os.makedirs(PROFILE_DIR, exist_ok=True)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@bp.route('', methods=['GET', 'POST'])
|
||||
def handle_profiles():
|
||||
if request.method == 'POST':
|
||||
data = request.json
|
||||
saved_data = save_profile(data)
|
||||
return jsonify(saved_data), 201
|
||||
else:
|
||||
profiles = load_all_profiles()
|
||||
return jsonify(profiles)
|
||||
|
||||
@bp.route('/<int:id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
def handle_profile(id):
|
||||
if request.method == 'GET':
|
||||
profile = load_profile(id)
|
||||
if profile:
|
||||
return jsonify(profile)
|
||||
return jsonify({"error": "Profile not found"}), 404
|
||||
elif request.method == 'PUT':
|
||||
data = request.json
|
||||
data['id'] = id
|
||||
saved_data = save_profile(data)
|
||||
return jsonify(saved_data)
|
||||
elif request.method == 'DELETE':
|
||||
if delete_profile(id):
|
||||
return jsonify({"message": f"Profile with ID {id} deleted."}), 200
|
||||
return jsonify({"error": f"Profile with ID {id} not found."}), 404
|
||||
|
||||
def save_profile(data):
|
||||
logger.info("Received data for saving profile: %s", data)
|
||||
|
||||
# Sanitize and extract necessary fields
|
||||
name = sanitize_input(data.get('name', ''))
|
||||
description = sanitize_input(data.get('description', ''))
|
||||
profile_id = data.get('id', None)
|
||||
|
||||
# Determine if this is a new profile or an existing one
|
||||
if profile_id == 0 or not profile_id:
|
||||
profile_id = get_next_id(PROFILE_DIR)
|
||||
logger.info("Assigned new profile ID: %d", profile_id)
|
||||
date_created = get_current_timestamp()
|
||||
else:
|
||||
existing_filename = os.path.join(PROFILE_DIR, f"{profile_id}.yml")
|
||||
if os.path.exists(existing_filename):
|
||||
existing_data = load_profile(profile_id)
|
||||
date_created = existing_data.get('date_created', get_current_timestamp())
|
||||
else:
|
||||
raise FileNotFoundError(f"No existing file found for ID: {profile_id}")
|
||||
|
||||
date_modified = get_current_timestamp()
|
||||
|
||||
# Process tags
|
||||
tags = [sanitize_input(tag) for tag in data.get('tags', [])]
|
||||
|
||||
# Construct the ordered data
|
||||
ordered_data = OrderedDict([
|
||||
('id', profile_id),
|
||||
('name', name),
|
||||
('description', description),
|
||||
('date_created', str(date_created)),
|
||||
('date_modified', str(date_modified)),
|
||||
('tags', tags)
|
||||
])
|
||||
|
||||
# Generate the filename using only the ID
|
||||
filename = os.path.join(PROFILE_DIR, f"{profile_id}.yml")
|
||||
|
||||
# Write to the file
|
||||
with open(filename, 'w') as file:
|
||||
yaml.dump(ordered_data, file, default_flow_style=False, Dumper=yaml.SafeDumper)
|
||||
|
||||
return ordered_data
|
||||
|
||||
def load_profile(id):
|
||||
filename = os.path.join(PROFILE_DIR, f"{id}.yml")
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r') as file:
|
||||
data = yaml.safe_load(file)
|
||||
return data
|
||||
return None
|
||||
|
||||
def load_all_profiles():
|
||||
profiles = []
|
||||
for filename in os.listdir(PROFILE_DIR):
|
||||
if filename.endswith('.yml'):
|
||||
with open(os.path.join(PROFILE_DIR, filename), 'r') as file:
|
||||
data = yaml.safe_load(file)
|
||||
profiles.append(data)
|
||||
return profiles
|
||||
|
||||
def delete_profile(id):
|
||||
filename = os.path.join(PROFILE_DIR, f"{id}.yml")
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
return True
|
||||
return False
|
||||
@@ -2,6 +2,7 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import RegexPage from "./components/regex/RegexPage";
|
||||
import FormatPage from "./components/format/FormatPage";
|
||||
import ProfilePage from "./components/profile/ProfilePage";
|
||||
import SettingsPage from "./components/settings/SettingsPage";
|
||||
import Navbar from "./components/ui/Navbar";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
@@ -26,6 +27,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/regex" element={<RegexPage />} />
|
||||
<Route path="/format" element={<FormatPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -244,3 +244,43 @@ export const getDiff = async (filePath) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProfiles = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/profile`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching profiles:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveProfile = async (profile) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/profile`, profile);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error saving profile:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateProfile = async (id, profile) => {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/profile/${id}`, profile);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteProfile = async (id) => {
|
||||
try {
|
||||
const response = await axios.delete(`${API_BASE_URL}/profile/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error deleting profile:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
69
frontend/src/components/profile/ProfileCard.jsx
Normal file
69
frontend/src/components/profile/ProfileCard.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
function unsanitize(text) {
|
||||
return text.replace(/\\:/g, ":").replace(/\\n/g, "\n");
|
||||
}
|
||||
|
||||
function ProfileCard({ profile, onEdit, onClone, showDate, formatDate }) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm cursor-pointer hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-500 transition-shadow"
|
||||
onClick={() => onEdit(profile)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-bold text-lg text-gray-800 dark:text-gray-200">
|
||||
{unsanitize(profile.name)}
|
||||
</h3>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClone(profile);
|
||||
}}
|
||||
className="relative group"
|
||||
>
|
||||
<img
|
||||
src="/clone.svg"
|
||||
alt="Clone"
|
||||
className="w-5 h-5 transition-transform transform group-hover:scale-125 group-hover:rotate-12 group-hover:-translate-y-1 group-hover:translate-x-1"
|
||||
/>
|
||||
<span className="absolute bottom-0 right-0 w-2 h-2 bg-green-500 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"></span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm mt-2 mb-2">
|
||||
{unsanitize(profile.description)}
|
||||
</p>
|
||||
{showDate && (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-xs mb-2">
|
||||
Modified: {formatDate(profile.date_modified)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap">
|
||||
{profile.tags &&
|
||||
profile.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 mb-1 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ProfileCard.propTypes = {
|
||||
profile: PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
date_modified: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}).isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onClone: PropTypes.func.isRequired,
|
||||
showDate: PropTypes.bool.isRequired,
|
||||
formatDate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProfileCard;
|
||||
180
frontend/src/components/profile/ProfileModal.jsx
Normal file
180
frontend/src/components/profile/ProfileModal.jsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { saveProfile, deleteProfile } from "../../api/api";
|
||||
import Modal from "../ui/Modal";
|
||||
|
||||
function unsanitize(text) {
|
||||
return text.replace(/\\:/g, ":").replace(/\\n/g, "\n");
|
||||
}
|
||||
|
||||
function ProfileModal({ profile = null, isOpen, onClose, onSave }) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [tags, setTags] = useState([]);
|
||||
const [newTag, setNewTag] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const initialProfileRef = useRef(profile);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (profile && profile.id !== 0) {
|
||||
initialProfileRef.current = profile;
|
||||
setName(unsanitize(profile.name));
|
||||
setDescription(unsanitize(profile.description));
|
||||
setTags(profile.tags ? profile.tags.map(unsanitize) : []);
|
||||
} else {
|
||||
initialProfileRef.current = null;
|
||||
setName(profile ? unsanitize(profile.name) : "");
|
||||
setDescription(profile ? unsanitize(profile.description) : "");
|
||||
setTags(profile ? profile.tags.map(unsanitize) : []);
|
||||
}
|
||||
setError("");
|
||||
setNewTag("");
|
||||
}
|
||||
}, [profile, isOpen]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) {
|
||||
setError("Name is required.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveProfile({
|
||||
id: profile ? profile.id : 0,
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
});
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error saving profile:", error);
|
||||
setError("Failed to save profile. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const confirmDeletion = window.confirm(
|
||||
"Are you sure you want to delete this profile?"
|
||||
);
|
||||
if (!confirmDeletion) return;
|
||||
|
||||
try {
|
||||
await deleteProfile(profile.id);
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error deleting profile:", error);
|
||||
setError("Failed to delete profile. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (newTag.trim() && !tags.includes(newTag.trim())) {
|
||||
setTags([...tags, newTag.trim()]);
|
||||
setNewTag("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = (tagToRemove) => {
|
||||
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={initialProfileRef.current ? "Edit Profile" : "Add Profile"}
|
||||
>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Profile Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter profile name"
|
||||
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Enter description"
|
||||
className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex flex-wrap mb-2">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="ml-1 text-xs"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
placeholder="Add a tag"
|
||||
className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddTag}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ProfileModal.propTypes = {
|
||||
profile: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
tags: PropTypes.arrayOf(PropTypes.string),
|
||||
}),
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProfileModal;
|
||||
122
frontend/src/components/profile/ProfilePage.jsx
Normal file
122
frontend/src/components/profile/ProfilePage.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ProfileCard from "./ProfileCard";
|
||||
import ProfileModal from "./ProfileModal";
|
||||
import AddNewCard from "../ui/AddNewCard";
|
||||
import { getProfiles } from "../../api/api";
|
||||
import FilterMenu from "../ui/FilterMenu";
|
||||
import SortMenu from "../ui/SortMenu";
|
||||
|
||||
function ProfilePage() {
|
||||
const [profiles, setProfiles] = useState([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedProfile, setSelectedProfile] = useState(null);
|
||||
const [sortBy, setSortBy] = useState("name");
|
||||
const [filterType, setFilterType] = useState("none");
|
||||
const [filterValue, setFilterValue] = useState("");
|
||||
const [allTags, setAllTags] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfiles();
|
||||
}, []);
|
||||
|
||||
const fetchProfiles = async () => {
|
||||
try {
|
||||
const fetchedProfiles = await getProfiles();
|
||||
setProfiles(fetchedProfiles);
|
||||
const tags = [
|
||||
...new Set(fetchedProfiles.flatMap((profile) => profile.tags || [])),
|
||||
];
|
||||
setAllTags(tags);
|
||||
} catch (error) {
|
||||
console.error("Error fetching profiles:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenModal = (profile = null) => {
|
||||
setSelectedProfile(profile);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedProfile(null);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveProfile = () => {
|
||||
fetchProfiles();
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
const handleCloneProfile = (profile) => {
|
||||
const clonedProfile = {
|
||||
...profile,
|
||||
id: 0, // Ensure the ID is 0 for a new entry
|
||||
name: `${profile.name} [COPY]`,
|
||||
};
|
||||
setSelectedProfile(clonedProfile);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
const sortedAndFilteredProfiles = profiles
|
||||
.filter((profile) => {
|
||||
if (filterType === "tag") {
|
||||
return profile.tags && profile.tags.includes(filterValue);
|
||||
}
|
||||
if (filterType === "date") {
|
||||
const profileDate = new Date(profile.date_modified);
|
||||
const filterDate = new Date(filterValue);
|
||||
return profileDate.toDateString() === filterDate.toDateString();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === "name") return a.name.localeCompare(b.name);
|
||||
if (sortBy === "dateCreated")
|
||||
return new Date(b.date_created) - new Date(a.date_created);
|
||||
if (sortBy === "dateModified")
|
||||
return new Date(b.date_modified) - new Date(a.date_modified);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Manage Profiles</h2>
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
|
||||
<FilterMenu
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
filterValue={filterValue}
|
||||
setFilterValue={setFilterValue}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-4">
|
||||
{sortedAndFilteredProfiles.map((profile) => (
|
||||
<ProfileCard
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEdit={() => handleOpenModal(profile)}
|
||||
onClone={handleCloneProfile}
|
||||
showDate={sortBy !== "name"}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
<AddNewCard onAdd={() => handleOpenModal()} />
|
||||
</div>
|
||||
<ProfileModal
|
||||
profile={selectedProfile}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveProfile}
|
||||
allTags={allTags}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfilePage;
|
||||
@@ -44,6 +44,7 @@ function Navbar({ darkMode, setDarkMode }) {
|
||||
const getActiveTab = (pathname) => {
|
||||
if (pathname.startsWith("/regex")) return "regex";
|
||||
if (pathname.startsWith("/format")) return "format";
|
||||
if (pathname.startsWith("/profile")) return "profile";
|
||||
if (pathname.startsWith("/settings")) return "settings";
|
||||
return "settings"; // default to settings if no match
|
||||
};
|
||||
@@ -78,7 +79,7 @@ function Navbar({ darkMode, setDarkMode }) {
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
Regex
|
||||
Regex Patterns
|
||||
</Link>
|
||||
<Link
|
||||
to="/format"
|
||||
@@ -89,7 +90,18 @@ function Navbar({ darkMode, setDarkMode }) {
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
Custom Format
|
||||
Custom Formats
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
ref={(el) => (tabsRef.current["profile"] = el)}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium relative z-10 ${
|
||||
activeTab === "profile"
|
||||
? "text-white"
|
||||
: "text-gray-300 hover:bg-gray-700 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
Profiles
|
||||
</Link>
|
||||
<Link
|
||||
to="/settings"
|
||||
|
||||
Reference in New Issue
Block a user