diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 613e2ee..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,43 +0,0 @@ -# Buildathon Submission Pull Request - -Thank you for submitting your project for the Build Beyond Limits 2.0 - powered by Valkey and Breeth AI, hosted by React Hyderabad. - -Please fill in the details below before raising your Pull Request. - ---- - -## Submission Details - -**Project Name:** -**Attendee Name:** -**Problem Statement Selected:** - -Choose one: - -- [ ] HR Cost Intelligence Engine -- [ ] Intelligent Slack Knowledge Base - ---- - -## Checklist - -Please confirm the following: - -- [ ] I have created my submission file inside the `submissions/` folder. -- [ ] My submission file follows this naming format: `projectname_attendeeName.md`. -- [ ] I have used the `SUBMISSION_TEMPLATE.md` format. -- [ ] I have added my GitHub project repository link. -- [ ] I have added my LinkedIn profile link. -- [ ] I have added my GitHub username. -- [ ] My project repository is public. -- [ ] I have not added API keys, passwords, tokens, or private credentials. -- [ ] I understand that my actual project code should be in my own GitHub repository, not in this repository. - ---- - -## Project Repository Link - -Add your project GitHub repository link here: - -```txt -PASTE_LINK_HERE \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 25412a4..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,84 +0,0 @@ - ---- - -# `CODE_OF_CONDUCT.md` - -```md -# Code of Conduct - -## Our Promise - -React Hyderabad is committed to creating a welcoming, respectful, and inclusive environment for everyone participating in this buildathon. - -This applies to all participants, mentors, organizers, volunteers, judges, and community members. - -We expect everyone to treat each other with kindness, patience, and respect. - ---- - -## Expected Behaviour - -All participants are expected to: - -- Be respectful towards others -- Collaborate in a healthy and professional way -- Give constructive feedback -- Respect different skill levels and backgrounds -- Help others when possible -- Follow the event and submission guidelines -- Keep the community safe and welcoming - ---- - -## Unacceptable Behaviour - -The following behaviour will not be tolerated: - -- Harassment or bullying -- Discrimination of any kind -- Offensive comments or personal attacks -- Inappropriate language or behaviour -- Sharing someone’s private information without permission -- Plagiarism or claiming someone else’s work as your own -- Misuse of GitHub issues, pull requests, or discussions -- Any behaviour that makes others feel unsafe or unwelcome - ---- - -## Project and Submission Ethics - -Participants are expected to submit their own work. - -You may use AI tools, open-source libraries, APIs, documentation, and online resources, but you should understand what you are building and be able to explain your solution. - -If you use existing code, templates, or external resources, please give proper credit wherever required. - -Do not submit copied projects as original work. - ---- - -## Reporting Issues - -If you experience or notice unacceptable behaviour, please reach out to the React Hyderabad organizing team. - -The organizing team will review the concern and take appropriate action. - ---- - -## Enforcement - -Depending on the situation, the organizing team may take actions such as: - -- Giving a warning -- Asking for corrective action -- Rejecting a submission -- Removing a participant from the event or repository -- Taking further action if required - ---- - -## Final Note - -This buildathon is about learning, building, collaboration, and community. - -Please help us keep it respectful, safe, and meaningful for everyone. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e69de29..0000000 diff --git a/PROBLEM_STATEMENTS.md b/PROBLEM_STATEMENTS.md deleted file mode 100644 index fc5a465..0000000 --- a/PROBLEM_STATEMENTS.md +++ /dev/null @@ -1,23 +0,0 @@ -Here's the link to access the problem statements - - -All the problem statements here - [https://github.com/opensource-for-valkey/valkey-community/tree/main/hackathons/hyd13_2026-06-20](https://github.com/opensource-for-valkey/valkey-community/tree/main/hackathons/hyd13_2026-06-20) - -There are three important criterias for building your project - - -1. You must use Valkey in your app. -Valkey Intro - [https://github.com/pnbrown/valkey-intro](https://github.com/pnbrown/valkey-intro) -2. You should use Breeth AI in your app. Documentation is available - [https://thebreeth.com/](https://thebreeth.com/) -3. Please use React.js as your frontend library. - - - -Please make sure you only pick the problem statements as described above! - -We will be sharing the credits link of Breeth AI when we go live! - - - -Please join the discord channel for getting the credits - [https://discord.gg/mPC3AeUC](https://discord.gg/mPC3AeUC) - - - diff --git a/README.md b/README.md deleted file mode 100644 index d14c3c6..0000000 --- a/README.md +++ /dev/null @@ -1,156 +0,0 @@ -# build-beyond-limits-2.0 -PR submissions for the Build Beyond Limits 2.0 - Powered by Valkey, Hosted by React Hyderabad -Credit Partner - https://thebreeth.com/ - -Instructions releasing live on 20th June, at 3 PM - -Join here - https://youtube.com/live/kTT2Kovs-y8?feature=share - - -# React Hyderabad x Valkey - -Welcome to the official open-source submission repository for the **Build Beyond Limits 2.0 - powered by Valkey and Breeth AI, and hosted by React Hyderabad**. - -This buildathon is focused on solving **real-world business problems using AI**. The goal is not just to write code, but to understand a problem, think from a business perspective, use the right tools, build a working solution, and present it clearly. - -This repository will act as the central place where all participant submissions are listed. - -Your actual project/code should be maintained in your own GitHub repository. This repo is only for submitting your project details. - ---- - -## How to Participate - -1. Fork this repository. - -2. Create your own project repository on GitHub. - -3. Pick one of the problem statements from `PROBLEM_STATEMENTS.md`. - -4. Build your project in your own GitHub repository. - -5. In your fork of this repository, go to the `submissions/` folder. - -6. Create a new markdown file using this naming format: - -```txt -projectname_attendeeName.md -``` - -OR - -```txt -projectname_teamName.md -``` - -Example: - -```txt -ai-sales-copilot_shlok-srivastava.md -``` - -7. Use `SUBMISSION_TEMPLATE.md` as the blueprint for your submission. - -8. Fill in all the required details: - - * Project name - * Your name - * Problem statement selected - * Project description - * Approach - * Tools and technologies used - * GitHub project link - * LinkedIn profile - * GitHub username - -9. Raise a Pull Request to this repository. - -10. Get your Pull Request merged. - ---- - -## Important Submission Note - -Please make sure your Pull Request is raised by today. - -Even if your project is incomplete, you should still submit your project details by creating a Pull Request. - -You can continue building and improving your actual project in your own GitHub repository after the submission PR is merged. - -The purpose of this repository is to make the hackathon open, transparent, and community-driven. - ---- - -## Submission Rules - -1. Each participant must submit their own project. - -2. Your actual project code should be in your own GitHub repository. - -3. Do not push your project code directly into this repository. - -4. This repository should only contain participant submission files. - -5. Each submission file must be added inside the `submissions/` folder. - -6. Use the correct file naming format: - -```txt -projectname_attendeeName.md -``` -OR -```txt -projectname_attendeeName.md -``` - -7. Make sure your project repository is public. - -8. Do not commit API keys, private tokens, passwords, or secret credentials in your project repository. - -9. If your project is still a work in progress, mention that clearly in your submission. - -10. Keep your submission clear, honest, and easy to understand. - ---- - -## Pull Request Title Format - -Please use this format for your Pull Request title: - -```txt -Submission: Project Name - Attendee Name/ Team Name -``` - -Example: - -```txt -Submission: AI Sales Copilot - Shlok Srivastava -``` - ---- - -## What Makes a Good Submission? - -A good submission should clearly explain: - -* What problem you selected -* What your project does -* Why your solution is useful -* How you approached the problem -* What AI tools or technologies you used -* What is already working -* What you plan to improve next - -The project does not have to be perfect. We care about your thought process, execution, learning, and ability to solve a real-world problem. - ---- - -## About the Build Beyond Limits 2.0 - -This hackathon is hosted by **React Hyderabad** in collaboration with **Valkey** and **Breeth AI**. - -The goal is to bring builders together to solve meaningful problems using AI, frontend, backend, cloud, automation, and modern development tools. - -Build with intent. -Solve with clarity. -Ship with confidence. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index edd9514..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,50 +0,0 @@ - ---- - -# `SECURITY.md` - -```md -# Security Policy - -This repository is used only for collecting hackathon project submissions. - -Participants should not add sensitive information to this repository or to their public project repositories. - ---- - -## Do Not Commit Secrets - -Please do not commit: - -- API keys -- Access tokens -- Passwords -- Private credentials -- `.env` files -- Database connection strings -- Cloud service credentials -- Personal identification documents -- Any confidential business data - ---- - -## If You Accidentally Commit a Secret - -If you accidentally commit a secret: - -1. Remove it from your repository immediately. -2. Rotate or revoke the exposed key/token from the provider dashboard. -3. Update your project with a safe placeholder value. -4. Inform the organizers if the secret was committed to this repository. - ---- - -## Recommended Practice - -Use environment variables for local development. - -Example: - -```txt -OPENAI_API_KEY=your_api_key_here -DATABASE_URL=your_database_url_here \ No newline at end of file diff --git a/SUBMISSION_TEMPLATE.md b/SUBMISSION_TEMPLATE.md deleted file mode 100644 index a6e1d6e..0000000 --- a/SUBMISSION_TEMPLATE.md +++ /dev/null @@ -1,183 +0,0 @@ -# Project Submission Template - -Create a new file inside the `submissions/` folder using this naming format: - -```txt -projectname_attendeeName.md -``` - -Example: - -```txt -ai-sales-copilot_shlok-srivastava.md -``` - -Copy the template below into your new file and fill in the details. - ---- - -# Project Name - -Write your project name here. - ---- - -## Attendee/Team Details - -Add all the names of all the team members if you are participating in teams. - -**Name:** Your full name -**GitHub Username:** Your GitHub username -**LinkedIn Profile:** Your LinkedIn profile link -**GitHub Project Repository:** Your project repository link - ---- - -## Problem Statement Selected - -Mention the title of the problem statement you selected. - -Choose one: - -```txt -Problem statement 1 -``` - -or - -```txt -Problem statement 2 -``` - ---- - -## Project Description - -Briefly explain what your project does. - -Try to answer: - -* What is the project about? -* Who is it for? -* What problem does it solve? -* How does it help the user? - -Write your answer here. - ---- - -## Approach - -Explain how you approached the problem. - -You can include: - -* How you understood the problem -* What user flow you designed -* What features you decided to build -* How AI is used in your solution -* What makes your approach useful or different - -Write your answer here. - ---- - -## Tech Stack and Tools Used - -Mention the tools, technologies, frameworks, and platforms you used. - -Example: - -**Frontend:** React, Next.js, HTML, CSS, Tailwind, Material UI -**Backend:** Node.js, Express, FastAPI, Django, Flask -**Database:** MongoDB, PostgreSQL, Firebase, Supabase -**AI Tools/API:** OpenAI, Gemini, Claude, LangChain, Hugging Face, local LLMs -**Cloud/Deployment:** Vercel, Netlify, Render, Railway, AWS, GCP -**Other Tools:** Cursor, GitHub Copilot, Figma, Postman, Docker - -Fill your details below: - -**Frontend:** -**Backend:** -**Database:** -**AI Tools/API:** -**Cloud/Deployment:** -**Other Tools:** - ---- - -## Key Features - -List the key features of your project. - -Example: - -1. Feature one -2. Feature two -3. Feature three - -Write your features here: - -1. -2. -3. - ---- - -## What is Working? - -Mention what is currently working in your project. - -This is important even if your project is incomplete. - -Write your answer here. - ---- - -## What is Still in Progress? - -Mention what you are still building or planning to improve. - -Write your answer here. - ---- - -## Screenshots or Demo - -Add screenshots, demo video link, or deployed project link if available. - -**Deployed Link:** -**Demo Video Link:** -**Screenshots:** - ---- - -## Challenges Faced - -Mention any challenges you faced while building this project. - -Write your answer here. - ---- - -## Learnings - -Mention what you learned while building this project. - -Write your answer here. - ---- - -## Future Improvements - -Mention what you would improve if you had more time. - -Write your answer here. - ---- - -## Final Note - -Use this space to add anything else you want mentors, judges, or the community to know about your project. - -Write your answer here. diff --git a/submissions/putYourSubmissionsHere.md b/submissions/putYourSubmissionsHere.md deleted file mode 100644 index e69de29..0000000 diff --git a/submissions/valkey-ecommerce-demo/HACKATHON.md b/submissions/valkey-ecommerce-demo/HACKATHON.md new file mode 100644 index 0000000..1eb4fd7 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/HACKATHON.md @@ -0,0 +1,1460 @@ +# Hackathon Challenge Areas + +This document provides a detailed technical breakdown of each challenge area for the **Build Beyond Limits** hackathon. Each section describes the subsystem, the Valkey data structures and modules to use, expected API contracts, and implementation guidance. + +Use the [valkey-bundle](https://github.com/valkey-io/valkey-bundle) Docker image which includes all modules (Search, JSON, Bloom, etc.): + +```bash +docker run -d --name valkey -p 6379:6379 valkey/valkey-bundle:9-alpine +``` + +--- + +## ID Strategy + +All entity IDs use **UUIDv7** (RFC 9562) with a **domain prefix** in the format `domain:uuidv7`. UUIDv7 embeds a Unix timestamp in the most significant bits, which provides: + +- **Monotonic ordering**: IDs are naturally sortable by creation time without a separate `createdAt` index. +- **K-sortable**: Valkey Sorted Sets with UUIDv7 as members are inherently time-ordered. +- **No coordination**: Generate IDs client-side without a central sequence. +- **Indexing efficiency**: Lexicographic sorting of UUIDv7 strings matches chronological order, making `ZRANGEBYLEX` and prefix scans efficient. +- **Self-describing**: The domain prefix makes IDs unambiguous — you always know what entity type an ID refers to without additional context. + +### Format + +``` +{domain}:{uuidv7} +``` + +| Domain | Entity | Example | +|--------|--------|---------| +| `user` | User account | `user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e` | +| `product` | Product | `product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f` | +| `category` | Category | `category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b` | +| `vendor` | Vendor/Seller | `vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f` | +| `order` | Order | `order:0192d4e8-5e6f-7c8d-8a0b-2c3d4e5f6a7b` | +| `addr` | Address | `addr:0192d4e1-a2c8-7d4b-8f3e-1a9b5c7d2e4f` | +| `ad` | Advertisement | `ad:0192d4e9-6f7a-7d8e-9b0c-3d4e5f6a7b8c` | +| `session` | Auth session | `session:0192d4f0-1a2b-7c3d-8e4f-5a6b7c8d9e0f` | + +### Example generation (Node.js): +```js +import { v7 as uuidv7 } from 'uuid'; // npm install uuid@^9 + +function createId(domain) { + return `${domain}:${uuidv7()}`; +} + +const userId = createId('user'); // "user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e" +const productId = createId('product'); // "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f" +const orderId = createId('order'); // "order:0192d4e8-5e6f-7c8d-8a0b-2c3d4e5f6a7b" +``` + +### Parsing +```js +function parseId(id) { + const colonIndex = id.indexOf(':'); + const domain = id.substring(0, colonIndex); + const uuid = id.substring(colonIndex + 1); + return { domain, uuid }; +} + +const { domain, uuid } = parseId('user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e'); +// domain = "user", uuid = "0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e" +``` + +--- + +## Sample Data Contracts + +The following JSON structures define the shared data contracts all teams should follow. This ensures frontend and backend teams can work in parallel with consistent schemas. + +All `id` fields follow the `domain:uuidv7` format. The monotonic property of UUIDv7 means you can sort entities by ID to get chronological order — no need for a separate timestamp index in most cases. The domain prefix makes every ID self-describing. + +### Users + +```json +{ + "id": "user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e", + "email": "priya.sharma@example.com", + "passwordHash": "$2b$12$LJ3m4x...", + "firstName": "Priya", + "lastName": "Sharma", + "phone": "+91-9876543210", + "avatar": "/assets/avatars/priya.jpg", + "role": "customer", + "addresses": [ + { + "id": "addr:0192d4e1-a2c8-7d4b-8f3e-1a9b5c7d2e4f", + "label": "Home", + "street": "42 MG Road, Banjara Hills", + "city": "Hyderabad", + "state": "Telangana", + "postalCode": "500034", + "country": "IN", + "lat": 17.4156, + "lng": 78.4347, + "isDefault": true + } + ], + "preferences": { + "currency": "INR", + "language": "en", + "notifications": true + }, + "createdAt": "2025-01-15T10:30:00Z", + "lastLoginAt": "2025-05-20T14:22:00Z" +} +``` + +### Categories + +```json +[ + { + "id": "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", + "name": "Electronics", + "slug": "electronics", + "icon": "desktop", + "parentId": null, + "children": ["category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", "category:0192d4e2-4c8d-7a2e-9f1b-3d5c7e8a0b4f", "category:0192d4e2-5d9e-7b3f-8a2c-4e6d1f9b0c5a", "category:0192d4e2-6eaf-7c4a-9b3d-5f7e2a0c1d6b"] + }, + { + "id": "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", + "name": "Smartphones", + "slug": "smartphones", + "icon": "device-mobile", + "parentId": "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", + "children": [] + }, + { + "id": "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a", + "name": "Fashion", + "slug": "fashion", + "icon": "t-shirt", + "parentId": null, + "children": ["category:0192d4e3-8c2d-7e5f-9b3a-0d4c6e7f1a2b", "category:0192d4e3-9d3e-7f6a-8c4b-1e5d7f0a2b3c", "category:0192d4e3-ae4f-7a7b-9d5c-2f6e8a1b3c4d"] + }, + { + "id": "category:0192d4e4-1a2b-7c3d-8e4f-5a6b7c8d9e0f", + "name": "Home & Kitchen", + "slug": "home-kitchen", + "icon": "house", + "parentId": null, + "children": ["category:0192d4e4-2b3c-7d4e-9f5a-6b7c8d9e0f1a", "category:0192d4e4-3c4d-7e5f-8a6b-7c8d9e0f1a2b", "category:0192d4e4-4d5e-7f6a-9b7c-8d9e0f1a2b3c"] + }, + { + "id": "category:0192d4e5-5e6f-7a7b-8c8d-9e0f1a2b3c4d", + "name": "Sports & Outdoors", + "slug": "sports-outdoors", + "icon": "basketball", + "parentId": null, + "children": ["category:0192d4e5-6f7a-7b8c-9d9e-0f1a2b3c4d5e", "category:0192d4e5-7a8b-7c9d-8e0f-1a2b3c4d5e6f", "category:0192d4e5-8b9c-7d0e-9f1a-2b3c4d5e6f7a"] + } +] +``` + +### Products + +```json +{ + "id": "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", + "sku": "ELEC-PHN-SAM-001", + "name": "Galaxy Ultra Pro 256GB", + "slug": "galaxy-ultra-pro-256gb", + "description": "Flagship smartphone with 200MP camera, 6.8\" AMOLED display, and 5000mAh battery.", + "shortDescription": "200MP camera, 6.8\" AMOLED, 5000mAh", + "categoryId": "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", + "vendorId": "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + "brand": "Samsung", + "price": { + "amount": 89999, + "currency": "INR", + "compareAt": 99999 + }, + "images": [ + { + "url": "/assets/products/galaxy-ultra-front.jpg", + "alt": "Galaxy Ultra Pro front view", + "isPrimary": true + }, + { + "url": "/assets/products/galaxy-ultra-back.jpg", + "alt": "Galaxy Ultra Pro back view", + "isPrimary": false + } + ], + "attributes": { + "color": "Phantom Black", + "storage": "256GB", + "ram": "12GB", + "display": "6.8 inch AMOLED", + "battery": "5000mAh", + "os": "Android 15" + }, + "tags": ["smartphone", "5g", "flagship", "camera", "samsung"], + "inventory": { + "quantity": 150, + "reserved": 12, + "warehouse": "HYD-WH-01" + }, + "ratings": { + "average": 4.6, + "count": 2341 + }, + "embedding": [0.023, -0.156, 0.891, 0.445, ...], + "status": "active", + "createdAt": "2025-03-01T08:00:00Z", + "updatedAt": "2025-05-18T12:30:00Z" +} +``` + +### Vendors + +```json +{ + "id": "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + "name": "TechWorld Electronics", + "slug": "techworld-electronics", + "email": "support@techworld.in", + "phone": "+91-4012345678", + "logo": "/assets/vendors/techworld-logo.png", + "rating": 4.7, + "totalProducts": 342, + "totalSales": 15420, + "address": { + "street": "Plot 15, HITEC City", + "city": "Hyderabad", + "state": "Telangana", + "postalCode": "500081", + "country": "IN", + "lat": 17.4435, + "lng": 78.3772 + }, + "verified": true, + "joinedAt": "2024-06-15T00:00:00Z" +} +``` + +### Orders + +```json +{ + "id": "order:0192d4e8-5e6f-7c8d-8a0b-2c3d4e5f6a7b", + "userId": "user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e", + "status": "confirmed", + "items": [ + { + "productId": "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", + "sku": "ELEC-PHN-SAM-001", + "name": "Galaxy Ultra Pro 256GB", + "quantity": 1, + "price": 89999, + "vendorId": "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f" + } + ], + "subtotal": 89999, + "discount": 5000, + "couponCode": "VALKEY10", + "tax": 15300, + "shipping": 0, + "total": 100299, + "shippingAddress": { + "street": "42 MG Road, Banjara Hills", + "city": "Hyderabad", + "state": "Telangana", + "postalCode": "500034", + "country": "IN", + "lat": 17.4156, + "lng": 78.4347 + }, + "payment": { + "method": "upi", + "transactionId": "txn_abc123", + "status": "captured" + }, + "delivery": { + "estimatedAt": "2025-05-25T18:00:00Z", + "trackingId": "DEL-HYD-98765", + "currentLat": 17.4300, + "currentLng": 78.4100, + "status": "in_transit" + }, + "createdAt": "2025-05-22T09:15:00Z", + "updatedAt": "2025-05-22T10:00:00Z" +} +``` + +--- + +## Challenge 1: User Authentication + +### Objective +Implement user registration, login, and session management using Valkey as the session store. + +### Technical Requirements + +- **Registration**: Hash passwords with bcrypt (cost factor 12+), store user JSON in Valkey using `JSON.SET`. +- **Login**: Validate credentials, generate a session token (UUID v4 or JWT), store it in Valkey with a TTL. +- **Session Management**: Use Valkey strings with expiration for session tokens. Refresh TTL on each authenticated request. +- **Logout**: Delete the session key from Valkey. + +### Valkey Commands & Data Structures + +``` +# Store user profile (key = userId, e.g. "user:0192d4e0-...") +JSON.SET {userId} $ '{...user JSON...}' + +# Create session (expires in 24 hours) +SET session:{token} {userId} EX 86400 + +# Validate session +GET session:{token} + +# Refresh session TTL +EXPIRE session:{token} 86400 + +# Logout +DEL session:{token} + +# Track failed login attempts (rate limiting) +INCR login_attempts:{email} +EXPIRE login_attempts:{email} 900 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/auth/register` | Create new user account | +| POST | `/api/auth/login` | Authenticate and return session token | +| POST | `/api/auth/logout` | Invalidate session | +| GET | `/api/auth/me` | Get current user from session | +| POST | `/api/auth/refresh` | Refresh session TTL | + +### Acceptance Criteria +- Passwords are never stored in plaintext +- Sessions expire after 24 hours of inactivity +- Failed login attempts are rate-limited (max 5 per 15 minutes) +- Concurrent sessions per user are tracked + +--- + +## Challenge 2: Product Catalog + +### Objective +Build a product catalog service that stores and retrieves products, categories, and vendor information using Valkey JSON. + +### Technical Requirements + +- **Product Storage**: Store each product as a JSON document using `JSON.SET`. Support partial updates with `JSON.SET` on nested paths. +- **Category Hierarchy**: Model parent-child category relationships. Support breadcrumb navigation. +- **Filtering**: Filter products by category, price range, brand, attributes, and vendor. +- **Pagination**: Implement cursor-based or offset pagination for product listings. + +### Valkey Commands & Data Structures + +``` +# Store product (key = productId, e.g. "product:0192d4e6-...") +JSON.SET {productId} $ '{...product JSON...}' + +# Get product +JSON.GET {productId} + +# Get specific fields +JSON.GET {productId} $.name $.price $.ratings + +# Update inventory +JSON.SET {productId} $.inventory.quantity 138 + +# Store category (key = categoryId, e.g. "category:0192d4e2-...") +JSON.SET {categoryId} $ '{...category JSON...}' + +# Category-to-products index (sorted by creation date) +ZADD category_products:{categoryId} {timestamp} {productId} + +# Get products in category (paginated, newest first) +ZREVRANGEBYSCORE category_products:{categoryId} +inf -inf LIMIT 0 20 + +# Brand index +SADD brand_products:{brand} {productId} + +# Price range index (score = price in paise) +ZADD price_index 8999900 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +ZRANGEBYSCORE price_index 5000000 10000000 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/products` | List products (with filters, pagination) | +| GET | `/api/products/:id` | Get single product | +| GET | `/api/categories` | List all categories (tree) | +| GET | `/api/categories/:id/products` | Products in category | +| GET | `/api/vendors/:id/products` | Products by vendor | +| POST | `/api/products` | Create product (vendor/admin) | +| PATCH | `/api/products/:id` | Update product fields | + +### Acceptance Criteria +- Products are stored as JSON documents with all fields from the contract +- Category tree supports 2+ levels of nesting +- Filtering by price range, brand, and category works correctly +- Pagination returns consistent results + +--- + +## Challenge 3: Shopping Cart with Coupons + +### Objective +Implement a persistent shopping cart with coupon/discount code support using Valkey Hashes and JSON. + +### Technical Requirements + +- **Cart Storage**: Each user has a cart stored as a Valkey Hash where field = productId and value = quantity, or as a JSON document for richer data. +- **Cart Operations**: Add item, remove item, update quantity, clear cart. +- **Coupon System**: Store coupon definitions with rules (percentage off, fixed amount, minimum order, expiry date, usage limits). +- **Coupon Validation**: Check expiry, minimum order value, usage count, and applicable categories. +- **Guest Carts**: Support anonymous carts tied to a session ID, merged on login. + +### Valkey Commands & Data Structures + +``` +# Cart as Hash (simple: productId -> quantity) +HSET cart:{userId} product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f 2 +HSET cart:{userId} product:0192d4e6-3d5f-7b8c-9e0a-1b2c3d4e5f6a 1 + +# Get full cart +HGETALL cart:{userId} + +# Update quantity +HSET cart:{userId} product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f 3 + +# Remove item +HDEL cart:{userId} product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Clear cart +DEL cart:{userId} + +# Cart expiry (abandon after 7 days) +EXPIRE cart:{userId} 604800 + +# Coupon definition +JSON.SET coupon:{code} $ '{ + "code": "VALKEY10", + "type": "percentage", + "value": 10, + "minOrderAmount": 50000, + "maxDiscount": 10000, + "validFrom": "2025-05-01T00:00:00Z", + "validUntil": "2025-06-30T23:59:59Z", + "usageLimit": 1000, + "usedCount": 342, + "applicableCategories": ["category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a"], + "active": true +}' + +# Track coupon usage per user +SADD coupon_used:{code} {userId} +SISMEMBER coupon_used:{code} {userId} + +# Increment global usage +JSON.NUMINCRBY coupon:{code} $.usedCount 1 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/cart` | Get current user's cart | +| POST | `/api/cart/items` | Add item to cart | +| PATCH | `/api/cart/items/:productId` | Update item quantity | +| DELETE | `/api/cart/items/:productId` | Remove item from cart | +| DELETE | `/api/cart` | Clear entire cart | +| POST | `/api/cart/coupon` | Apply coupon code | +| DELETE | `/api/cart/coupon` | Remove applied coupon | + +### Acceptance Criteria +- Cart persists across browser sessions for logged-in users +- Guest carts merge into user cart on login +- Coupons validate all rules before applying +- A user cannot use the same coupon twice +- Cart shows real-time price calculations with discount + +--- + +## Challenge 4: Trending Products + +### Objective +Track product views, purchases, and interactions in real-time to surface trending products using Valkey Sorted Sets. + +### Technical Requirements + +- **Event Tracking**: Record product views, add-to-cart events, and purchases with timestamps. +- **Trending Score**: Calculate a weighted score: `views * 1 + addToCart * 3 + purchases * 5`. +- **Time Decay**: Trending scores should decay over time. Use sliding windows (last 1 hour, 6 hours, 24 hours). +- **Category Trending**: Track trending per category in addition to global trending. + +### Valkey Commands & Data Structures + +``` +# Increment product score on view +ZINCRBY trending:global:1h 1 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +ZINCRBY trending:global:24h 1 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Increment on add-to-cart +ZINCRBY trending:global:1h 3 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +ZINCRBY trending:global:24h 3 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Increment on purchase +ZINCRBY trending:global:1h 5 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +ZINCRBY trending:global:24h 5 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Category-specific trending +ZINCRBY trending:category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b:1h 1 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Get top 10 trending products +ZREVRANGE trending:global:1h 0 9 WITHSCORES + +# Time-series for granular analytics (Sorted Set: score = timestamp, member = productId) +ZADD product_views:{date} {timestamp} {productId} +# Count views in last hour +ZCOUNT product_views:{date} {timestamp - 3600} {timestamp} + +# Expire hourly buckets (auto-cleanup) +EXPIRE trending:global:1h 3600 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/trending` | Get global trending products | +| GET | `/api/trending/:categoryId` | Trending in category | +| POST | `/api/events/view` | Record product view | +| POST | `/api/events/add-to-cart` | Record add-to-cart | +| POST | `/api/events/purchase` | Record purchase event | + +### Acceptance Criteria +- Trending list updates in real-time as events come in +- Different time windows return different results +- Scores reflect weighted interactions (purchase > add-to-cart > view) +- Stale products naturally fall off trending lists + +--- + +## Challenge 5: Ads + +### Objective +Build an ad placement and targeting system that serves relevant ads based on user context (current category, search query, browsing history) using Valkey. + +### Technical Requirements + +- **Ad Storage**: Store ad creatives with targeting rules (categories, keywords, user segments). +- **Ad Selection**: Given a context (page, category, search term), select the most relevant ad with highest bid. +- **Impression Tracking**: Count impressions and clicks per ad. Enforce daily budget limits. +- **Frequency Capping**: Limit how many times a user sees the same ad per session/day. + +### Valkey Commands & Data Structures + +``` +# Store ad creative (key = adId, e.g. "ad:0192d4e9-...") +JSON.SET {adId} $ '{ + "id": "ad:0192d4e9-6f7a-7d8e-9b0c-3d4e5f6a7b8c", + "vendorId": "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + "title": "Summer Electronics Sale", + "imageUrl": "/assets/ads/summer-sale.jpg", + "targetUrl": "/shop?sale=summer", + "targetCategories": ["category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c"], + "targetKeywords": ["phone", "laptop", "gadget"], + "bidAmount": 500, + "dailyBudget": 50000, + "status": "active" +}' + +# Index ads by category (sorted by bid) +ZADD ads:category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b 500 ad:0192d4e9-6f7a-7d8e-9b0c-3d4e5f6a7b8c + +# Track impressions today +INCR ad_impressions:{adId}:{date} +EXPIRE ad_impressions:{adId}:{date} 86400 + +# Track clicks +INCR ad_clicks:{adId}:{date} + +# Frequency cap per user per day +INCR ad_freq:{userId}:{adId}:{date} +EXPIRE ad_freq:{userId}:{adId}:{date} 86400 + +# Daily spend tracking +INCRBY ad_spend:{adId}:{date} {bidAmount} +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/ads?context=category&value=category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b` | Get relevant ads | +| POST | `/api/ads/:adId/impression` | Record impression | +| POST | `/api/ads/:adId/click` | Record click | +| POST | `/api/ads` | Create ad (vendor) | +| GET | `/api/ads/:adId/stats` | Get ad performance | + +### Acceptance Criteria +- Ads are served based on relevance to current page context +- Higher bids get priority placement +- Ads stop serving when daily budget is exhausted +- Users don't see the same ad more than 3 times per session +- Click-through rate (CTR) is trackable + +--- + +## Challenge 6: Full-Text Search + +### Objective +Implement product search with full-text capabilities using Valkey Search module. Support autocomplete, typo tolerance, and faceted filtering. + +### Technical Requirements + +- **Index Creation**: Create a search index on product JSON documents covering name, description, brand, tags, category, and price. +- **Query Support**: Full-text queries with stemming, prefix matching, and fuzzy matching. +- **Faceted Search**: Return aggregated counts by category, brand, and price range alongside results. +- **Autocomplete**: Provide search suggestions as the user types (prefix-based). +- **Sorting**: Support sorting by relevance, price (asc/desc), rating, and newest. + +### Valkey Commands & Data Structures + +``` +# Create search index on JSON documents +FT.CREATE idx:products ON JSON PREFIX 1 product: + SCHEMA + $.name AS name TEXT WEIGHT 5.0 + $.description AS description TEXT WEIGHT 1.0 + $.brand AS brand TEXT WEIGHT 3.0 SORTABLE + $.tags AS tags TAG SEPARATOR "," + $.categoryId AS categoryId TAG + $.price.amount AS price NUMERIC SORTABLE + $.ratings.average AS rating NUMERIC SORTABLE + $.status AS status TAG + $.createdAt AS createdAt TEXT SORTABLE + +# Full-text search +FT.SEARCH idx:products "galaxy phone" LIMIT 0 20 + +# Search with filters +FT.SEARCH idx:products "@name:galaxy @categoryId:{0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c} @price:[50000 100000]" + SORTBY price ASC + LIMIT 0 20 + +# Fuzzy search (typo tolerance) +FT.SEARCH idx:products "%%galxy%%" LIMIT 0 10 + +# Faceted aggregation (count by brand) +FT.AGGREGATE idx:products "phone" + GROUPBY 1 @brand + REDUCE COUNT 0 AS count + SORTBY 2 @count DESC + +# Autocomplete suggestions +FT.SUGADD autocomplete "Galaxy Ultra Pro" 100 +FT.SUGADD autocomplete "Gaming Laptop" 85 +FT.SUGGET autocomplete "gal" FUZZY MAX 5 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/search?q=galaxy&category=category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c&minPrice=50000&maxPrice=100000&sort=price_asc&page=1` | Search products | +| GET | `/api/search/suggest?q=gal` | Autocomplete suggestions | +| GET | `/api/search/facets?q=phone` | Get facet counts | + +### Response Format + +```json +{ + "query": "galaxy phone", + "total": 47, + "page": 1, + "pageSize": 20, + "results": [ + { "id": "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", "name": "Galaxy Ultra Pro 256GB", "score": 0.95, "...": "..." } + ], + "facets": { + "brands": [{ "name": "Samsung", "count": 23 }, { "name": "Apple", "count": 12 }], + "priceRanges": [{ "range": "50000-100000", "count": 18 }], + "categories": [{ "id": "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", "name": "Smartphones", "count": 35 }] + } +} +``` + +### Acceptance Criteria +- Search returns relevant results within 50ms +- Typos in queries still return useful results +- Facets accurately reflect the filtered result set +- Autocomplete responds within 20ms +- Results can be sorted by multiple criteria + +--- + +## Challenge 7: Vector Similarity Search + +### Objective +Implement semantic product search using vector embeddings stored in Valkey. Users can search by meaning rather than exact keywords (e.g., "something to keep my coffee warm" → finds thermoses and insulated mugs). + +### Technical Requirements + +- **Embedding Generation**: Generate vector embeddings for product descriptions using a pre-trained model (e.g., sentence-transformers, OpenAI embeddings). Store as FLOAT32 vectors. +- **Vector Index**: Create a vector similarity index using HNSW or FLAT algorithm. +- **Hybrid Search**: Combine vector similarity with traditional filters (category, price range). +- **Similar Products**: Given a product, find visually or semantically similar products. + +### Valkey Commands & Data Structures + +``` +# Create vector index on product embeddings +FT.CREATE idx:product_vectors ON JSON PREFIX 1 product: + SCHEMA + $.name AS name TEXT + $.categoryId AS categoryId TAG + $.price.amount AS price NUMERIC + $.embedding AS embedding VECTOR HNSW 6 + TYPE FLOAT32 + DIM 384 + DISTANCE_METRIC COSINE + +# KNN search (find 10 most similar products to query vector) +FT.SEARCH idx:product_vectors + "*=>[KNN 10 @embedding $query_vec AS score]" + PARAMS 2 query_vec "\x00\x00..." + SORTBY score + RETURN 4 name score price categoryId + DIALECT 2 + +# Hybrid: vector search + category filter +FT.SEARCH idx:product_vectors + "(@categoryId:{0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b})=>[KNN 10 @embedding $query_vec AS score]" + PARAMS 2 query_vec "\x00\x00..." + SORTBY score + DIALECT 2 + +# Similar products (use product's own embedding as query) +# 1. Get the product embedding +JSON.GET product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f $.embedding +# 2. Use it as query vector in KNN search +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/search/semantic?q=keep coffee warm` | Semantic search | +| GET | `/api/products/:id/similar` | Find similar products | +| POST | `/api/search/by-image` | Search by image embedding | + +### Acceptance Criteria +- Semantic queries return contextually relevant results even without keyword matches +- Vector search completes within 100ms for 10K+ products +- Similar products endpoint returns genuinely related items +- Hybrid search correctly applies filters on top of vector results + +--- + +## Challenge 8: Analytics with Prometheus + +### Objective +Instrument the application with metrics and expose them in Prometheus format. Track business KPIs and system performance using Valkey core data structures for real-time dashboards. + +### Technical Requirements + +- **Business Metrics**: Track orders per minute, revenue per hour, active users, cart abandonment rate. +- **System Metrics**: Track API response times, Valkey command latency, error rates. +- **Metrics Storage**: Use Valkey Sorted Sets (score = timestamp), Hashes, and HyperLogLog for metrics aggregation. +- **Prometheus Endpoint**: Expose `/metrics` in Prometheus exposition format. +- **Time Bucketing**: Use time-bucketed keys (per minute, per hour) with TTL for automatic cleanup. + +### Valkey Commands & Data Structures + +``` +# Orders counter (per-minute bucket, auto-expires) +INCR metrics:orders:count:{minute_timestamp} +EXPIRE metrics:orders:count:{minute_timestamp} 86400 + +# Revenue tracking (per-hour bucket) +INCRBY metrics:revenue:{hour_timestamp} 100299 +EXPIRE metrics:revenue:{hour_timestamp} 604800 + +# API latency tracking (Sorted Set: score = latency, member = requestId) +ZADD metrics:api_latency:{minute_timestamp} 45 {requestId} +EXPIRE metrics:api_latency:{minute_timestamp} 3600 + +# Calculate percentiles from sorted set +ZCARD metrics:api_latency:{minute_timestamp} # total count +ZRANGEBYSCORE metrics:api_latency:{minute_timestamp} -inf +inf # all values sorted +# p50 = element at index count/2, p95 = element at index count*0.95 + +# Active users (HyperLogLog for unique count) +PFADD active_users:{date}:{hour} {userId} +PFCOUNT active_users:{date}:{hour} + +# Gauge-style metrics (current value in a Hash) +HSET metrics:gauges cart_abandonment_rate 0.23 +HSET metrics:gauges active_sessions 1542 +HSET metrics:gauges items_in_stock 48230 + +# Counter-style metrics (atomic increment) +INCR metrics:counters:http_requests:{status_code}:{minute_timestamp} +INCR metrics:counters:valkey_commands:{minute_timestamp} +INCR metrics:counters:errors:{service}:{minute_timestamp} + +# Query metrics for a time range (scan matching keys) +# Application code iterates over minute buckets between from/to timestamps +# e.g., GET metrics:orders:count:1716372600, GET metrics:orders:count:1716372660, ... + +# Histogram buckets for latency distribution +HINCRBY metrics:latency_histogram:{minute_timestamp} "le_10" 1 # <= 10ms +HINCRBY metrics:latency_histogram:{minute_timestamp} "le_50" 1 # <= 50ms +HINCRBY metrics:latency_histogram:{minute_timestamp} "le_100" 1 # <= 100ms +HINCRBY metrics:latency_histogram:{minute_timestamp} "le_500" 1 # <= 500ms +HINCRBY metrics:latency_histogram:{minute_timestamp} "le_inf" 1 # <= +Inf +EXPIRE metrics:latency_histogram:{minute_timestamp} 86400 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/metrics` | Prometheus exposition format | +| GET | `/api/analytics/dashboard` | Real-time dashboard data | +| GET | `/api/analytics/revenue?from=&to=&interval=1h` | Revenue over time | +| GET | `/api/analytics/orders?from=&to=` | Order count over time | +| GET | `/api/analytics/active-users` | Unique active users | + +### Prometheus Exposition Format Example + +``` +# HELP http_requests_total Total HTTP requests +# TYPE http_requests_total counter +http_requests_total{method="GET",status="200"} 1542 +http_requests_total{method="POST",status="201"} 312 + +# HELP http_request_duration_seconds HTTP request latency +# TYPE http_request_duration_seconds histogram +http_request_duration_seconds_bucket{le="0.01"} 800 +http_request_duration_seconds_bucket{le="0.05"} 1200 +http_request_duration_seconds_bucket{le="0.1"} 1400 +http_request_duration_seconds_bucket{le="0.5"} 1520 +http_request_duration_seconds_bucket{le="+Inf"} 1542 + +# HELP active_users_total Current unique active users +# TYPE active_users_total gauge +active_users_total 1542 +``` + +### Acceptance Criteria +- Prometheus can scrape the `/metrics` endpoint successfully +- Metrics are bucketed by time with automatic TTL-based cleanup +- Dashboard shows real-time updates (< 5 second delay) +- At least 5 business metrics and 3 system metrics are tracked +- Latency percentiles (p50, p95, p99) are calculable from stored data + +--- + +## Challenge 9: Observability with OpenSearch + +### Objective +Implement structured logging, distributed tracing, and error tracking. Forward logs to OpenSearch for centralized search and visualization. + +### Technical Requirements + +- **Structured Logging**: All logs in JSON format with correlation IDs, timestamps, severity levels, and context. +- **Request Tracing**: Assign a trace ID to each incoming request. Propagate through all internal calls. +- **Error Tracking**: Capture and aggregate errors with stack traces, frequency, and affected users. +- **Log Buffering**: Use Valkey Streams as a buffer before forwarding to OpenSearch (prevents log loss during OpenSearch downtime). + +### Valkey Commands & Data Structures + +``` +# Buffer logs in a Valkey Stream +XADD logs:app * level "error" service "cart-service" traceId "trc_abc123" + message "Failed to apply coupon" userId "user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e" + timestamp "2025-05-22T09:15:00Z" stack "Error: Invalid coupon..." + +# Read logs (consumer group for reliable processing) +XGROUP CREATE logs:app log-forwarder $ MKSTREAM +XREADGROUP GROUP log-forwarder worker-1 COUNT 100 BLOCK 5000 STREAMS logs:app > + +# Acknowledge processed logs +XACK logs:app log-forwarder {messageId} + +# Trim old logs (keep last 100K entries) +XTRIM logs:app MAXLEN ~ 100000 + +# Error aggregation +ZINCRBY errors:24h 1 "cart-service:InvalidCoupon" +EXPIRE errors:24h 86400 + +# Request tracing (store span data) +JSON.SET trace:{traceId} $ '{ + "traceId": "trc_abc123", + "spans": [ + {"service": "api-gateway", "operation": "POST /cart/coupon", "duration": 45}, + {"service": "cart-service", "operation": "validateCoupon", "duration": 12}, + {"service": "valkey", "operation": "JSON.GET coupon:VALKEY10", "duration": 2} + ], + "totalDuration": 45, + "status": "error" +}' +EXPIRE trace:{traceId} 3600 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/observability/logs?level=error&service=cart-service&limit=50` | Query recent logs | +| GET | `/api/observability/traces/:traceId` | Get full trace | +| GET | `/api/observability/errors` | Top errors (last 24h) | +| GET | `/api/observability/health` | Service health status | + +### Acceptance Criteria +- All API requests have a unique trace ID in response headers (`X-Trace-Id`) +- Logs are buffered in Valkey Streams and forwarded to OpenSearch +- No logs are lost during brief OpenSearch outages +- Errors are aggregated and ranked by frequency +- Trace data shows the full request lifecycle across services + +--- + +## Challenge 10: Checkout with Inventory Tracking + +### Objective +Implement a checkout flow with atomic inventory reservation, payment processing, and order creation. Prevent overselling using Valkey transactions. + +### Technical Requirements + +- **Inventory Reservation**: When a user starts checkout, reserve inventory atomically. Release if checkout is abandoned (TTL-based). +- **Atomic Decrement**: Use Lua scripts or transactions to ensure inventory never goes negative. +- **Order Creation**: Create order document on successful payment. Update inventory permanently. +- **Idempotency**: Prevent duplicate orders from double-clicks or retries using idempotency keys. + +### Valkey Commands & Data Structures + +``` +# Atomic inventory check-and-reserve (Lua script) +# KEYS[1] = product inventory key, ARGV[1] = quantity +EVAL " + local current = tonumber(redis.call('JSON.GET', KEYS[1], '$.inventory.quantity')) + local reserved = tonumber(redis.call('JSON.GET', KEYS[1], '$.inventory.reserved')) + local available = current - reserved + if available >= tonumber(ARGV[1]) then + redis.call('JSON.NUMINCRBY', KEYS[1], '$.inventory.reserved', ARGV[1]) + return 1 + end + return 0 +" 1 product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f 2 + +# Reservation with TTL (auto-release after 10 minutes) +SET reservation:{orderId}:{productId} {quantity} EX 600 + +# On successful payment: commit inventory +JSON.NUMINCRBY {productId} $.inventory.quantity -{quantity} +JSON.NUMINCRBY {productId} $.inventory.reserved -{quantity} +DEL reservation:{orderId}:{productId} + +# On abandoned checkout: release reservation +# (handled by TTL expiry + keyspace notification) + +# Idempotency key +SET idempotency:{key} {orderId} NX EX 86400 + +# Store order +JSON.SET {orderId} $ '{...order JSON...}' + +# Order history index +ZADD user_orders:{userId} {timestamp} {orderId} +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/checkout/start` | Reserve inventory, create pending order | +| POST | `/api/checkout/payment` | Process payment | +| POST | `/api/checkout/confirm` | Confirm order after payment | +| POST | `/api/checkout/cancel` | Cancel and release inventory | +| GET | `/api/orders` | User's order history | +| GET | `/api/orders/:id` | Order details | + +### Acceptance Criteria +- Inventory never goes negative even under concurrent checkouts +- Abandoned reservations auto-release after 10 minutes +- Duplicate payment submissions are safely rejected +- Order is only created after successful payment confirmation +- Full order history is queryable per user + +--- + +## Challenge 11: Delivery with Geolocation + +### Objective +Implement delivery tracking with real-time geolocation updates using Valkey Geospatial commands. Support delivery radius queries and ETA calculations. + +### Technical Requirements + +- **Geospatial Indexing**: Store delivery agent locations and warehouse positions using Valkey GEO commands. +- **Real-time Updates**: Delivery agents push location updates every 10 seconds. +- **Proximity Queries**: Find nearest warehouse to customer, nearest delivery agent to warehouse. +- **ETA Calculation**: Estimate delivery time based on distance and average speed. +- **Delivery Zones**: Define serviceable areas and validate delivery addresses. + +### Valkey Commands & Data Structures + +``` +# Store warehouse locations +GEOADD warehouses 78.4347 17.4156 "HYD-WH-01" +GEOADD warehouses 78.3772 17.4435 "HYD-WH-02" +GEOADD warehouses 78.4867 17.3850 "HYD-WH-03" + +# Store delivery agent locations (updated every 10s) +GEOADD delivery_agents 78.4100 17.4300 "agent_raj_001" +GEOADD delivery_agents 78.4500 17.4200 "agent_kumar_002" + +# Find nearest warehouse to customer (within 15km) +GEOSEARCH warehouses FROMLONLAT 78.4347 17.4156 BYRADIUS 15 km ASC COUNT 3 + +# Find nearest delivery agent to warehouse +GEOSEARCH delivery_agents FROMLONLAT 78.4347 17.4156 BYRADIUS 5 km ASC COUNT 1 + +# Distance between agent and delivery address +GEODIST delivery_agents "agent_raj_001" "customer_location" km + +# Store delivery tracking info (key = trackingId) +JSON.SET {trackingId} $ '{ + "trackingId": "DEL-HYD-98765", + "orderId": "order:0192d4e8-5e6f-7c8d-8a0b-2c3d4e5f6a7b", + "agentId": "agent_raj_001", + "status": "in_transit", + "pickupLocation": {"lat": 17.4156, "lng": 78.4347}, + "dropLocation": {"lat": 17.4300, "lng": 78.4100}, + "currentLocation": {"lat": 17.4200, "lng": 78.4200}, + "estimatedArrival": "2025-05-22T11:30:00Z", + "history": [ + {"status": "picked_up", "timestamp": "2025-05-22T10:00:00Z", "lat": 17.4156, "lng": 78.4347}, + {"status": "in_transit", "timestamp": "2025-05-22T10:15:00Z", "lat": 17.4200, "lng": 78.4200} + ] +}' + +# Pub/Sub for real-time location updates to frontend +PUBLISH delivery:location:{trackingId} '{"lat": 17.4250, "lng": 78.4150, "eta": "11:25"}' +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/delivery/:trackingId` | Get delivery status and location | +| GET | `/api/delivery/:trackingId/track` | SSE stream for real-time updates | +| POST | `/api/delivery/:trackingId/location` | Agent pushes location update | +| GET | `/api/delivery/check-serviceability?lat=&lng=` | Check if address is serviceable | +| GET | `/api/delivery/eta?from=&to=` | Estimate delivery time | + +### Acceptance Criteria +- Delivery location updates in real-time on the frontend map +- Nearest warehouse selection is correct based on customer location +- ETA updates as delivery agent moves +- Non-serviceable areas are correctly rejected +- Location history is preserved for the full delivery lifecycle + +--- + +## Challenge 12: Rate Limiting + +### Objective +Implement API rate limiting to protect services from abuse. Support multiple strategies: fixed window, sliding window, and token bucket. + +### Technical Requirements + +- **Per-User Limits**: Authenticated users get higher limits than anonymous users. +- **Per-Endpoint Limits**: Different endpoints have different rate limits (e.g., search: 30/min, checkout: 5/min). +- **Sliding Window**: Use a sliding window algorithm for smooth rate limiting without burst edges. +- **Response Headers**: Return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers. +- **Graceful Degradation**: Return 429 Too Many Requests with retry-after information. + +### Valkey Commands & Data Structures + +``` +# Fixed Window (simple counter) +INCR ratelimit:{userId}:{endpoint}:{window} +EXPIRE ratelimit:{userId}:{endpoint}:{window} 60 +# Check: if count > limit, reject + +# Sliding Window Log (precise but memory-intensive) +ZADD ratelimit:sliding:{userId}:{endpoint} {timestamp} {requestId} +ZREMRANGEBYSCORE ratelimit:sliding:{userId}:{endpoint} 0 {timestamp - windowSize} +ZCARD ratelimit:sliding:{userId}:{endpoint} +# Check: if count > limit, reject + +# Token Bucket (Lua script for atomicity) +EVAL " + local key = KEYS[1] + local now = tonumber(ARGV[1]) + local rate = tonumber(ARGV[2]) + local capacity = tonumber(ARGV[3]) + local requested = tonumber(ARGV[4]) + + local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') + local tokens = tonumber(bucket[1]) or capacity + local last_refill = tonumber(bucket[2]) or now + + local elapsed = now - last_refill + local refill = math.floor(elapsed * rate) + tokens = math.min(capacity, tokens + refill) + + if tokens >= requested then + tokens = tokens - requested + redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now) + redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1) + return {1, tokens} + end + return {0, tokens} +" 1 ratelimit:bucket:{userId}:{endpoint} {now} {rate} {capacity} 1 + +# IP-based rate limiting for anonymous users +INCR ratelimit:ip:{clientIp}:{endpoint}:{minute} +EXPIRE ratelimit:ip:{clientIp}:{endpoint}:{minute} 60 +``` + +### Rate Limit Configuration + +```json +{ + "endpoints": { + "/api/search": { "anonymous": 20, "authenticated": 60, "window": 60 }, + "/api/checkout/start": { "anonymous": 0, "authenticated": 5, "window": 60 }, + "/api/auth/login": { "anonymous": 5, "authenticated": 5, "window": 900 }, + "/api/products": { "anonymous": 30, "authenticated": 100, "window": 60 }, + "/api/cart/*": { "anonymous": 10, "authenticated": 30, "window": 60 } + } +} +``` + +### API Response Headers + +``` +HTTP/1.1 429 Too Many Requests +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 0 +X-RateLimit-Reset: 1716372600 +Retry-After: 45 +Content-Type: application/json + +{"error": "rate_limit_exceeded", "message": "Too many requests. Try again in 45 seconds."} +``` + +### Acceptance Criteria +- Rate limits are enforced per user and per endpoint +- Anonymous users have stricter limits than authenticated users +- Response headers accurately reflect remaining quota +- 429 responses include actionable retry information +- Rate limiting does not add more than 2ms latency to requests + +--- + +## Challenge 13: Real-time Recommendations + +### Objective +Build a recommendation engine that suggests products based on user behavior (collaborative filtering), browsing history, and purchase patterns using Valkey data structures. + +### Technical Requirements + +- **Browsing History**: Track recently viewed products per user. Use this for "Recently Viewed" and "Continue Shopping" sections. +- **Collaborative Filtering**: "Users who bought X also bought Y" — use co-purchase matrices stored in Sorted Sets. +- **Category Affinity**: Track which categories a user browses most. Weight recommendations toward preferred categories. +- **Real-time Updates**: Recommendations update immediately as new interactions occur. +- **Diversity**: Avoid recommending the same items repeatedly. Mix familiar categories with discovery. + +### Valkey Commands & Data Structures + +``` +# Recently viewed (capped list, newest first) +LPUSH user_history:{userId} product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +LTRIM user_history:{userId} 0 49 + +# Co-purchase matrix: "bought with" relationships +# When user buys productA and productB together: +ZINCRBY copurchase:{productA} 1 {productB} +ZINCRBY copurchase:{productB} 1 {productA} + +# Get top co-purchased products +ZREVRANGE copurchase:{productId} 0 9 WITHSCORES + +# Category affinity (weighted by recency and action type) +ZINCRBY user_affinity:{userId} 1 category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b # view +ZINCRBY user_affinity:{userId} 3 category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b # add to cart +ZINCRBY user_affinity:{userId} 5 category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b # purchase + +# Get user's top categories +ZREVRANGE user_affinity:{userId} 0 4 WITHSCORES + +# "Frequently bought together" for product page +ZREVRANGE copurchase:product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f 0 4 + +# Personalized feed: combine affinity + trending + history +# Use ZUNIONSTORE with weights for blended scoring +ZUNIONSTORE recommendations:{userId} 3 + user_affinity:{userId} + trending:global:24h + copurchase:{lastViewedProduct} + WEIGHTS 3 1 2 + +# Exclude already purchased +SDIFF recommendations:{userId} user_purchased:{userId} + +# Session-based recommendations (for anonymous users) +LPUSH session_history:{sessionId} product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f +EXPIRE session_history:{sessionId} 1800 +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/recommendations/personalized` | Personalized product feed | +| GET | `/api/recommendations/similar/:productId` | "Customers also bought" | +| GET | `/api/recommendations/recently-viewed` | User's browsing history | +| GET | `/api/recommendations/trending-for-you` | Trending in user's preferred categories | +| POST | `/api/recommendations/events` | Record interaction event | + +### Acceptance Criteria +- Recommendations are personalized based on actual user behavior +- New interactions immediately influence recommendations +- "Bought together" suggestions are based on real co-purchase data +- Anonymous users get session-based recommendations +- Already-purchased items are excluded from suggestions + +--- + +## Challenge 14: Agentic Search + +### Objective +Build an AI-powered search experience that understands natural language queries, maintains conversation context, and can perform multi-step reasoning to find products. Combine full-text search, vector search, and structured filters into an intelligent agent. + +### Technical Requirements + +- **Natural Language Understanding**: Parse queries like "I need a birthday gift for my 10-year-old nephew who likes science" into structured search parameters. +- **Multi-step Reasoning**: The agent can refine results by asking clarifying questions or combining multiple searches. +- **Conversation Memory**: Maintain search context across multiple turns using Valkey (e.g., "show me cheaper options" refers to the previous search). +- **Tool Use**: The agent has access to tools: `search_products`, `filter_by_price`, `get_reviews`, `check_availability`, `get_similar`. +- **Result Explanation**: Explain why each product was recommended in the context of the user's query. + +### Valkey Commands & Data Structures + +``` +# Store conversation context +JSON.SET conversation:{sessionId} $ '{ + "sessionId": "sess_abc123", + "userId": "user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e", + "turns": [ + { + "role": "user", + "content": "I need a birthday gift for my 10-year-old nephew who likes science", + "timestamp": "2025-05-22T10:00:00Z" + }, + { + "role": "agent", + "content": "Here are some science-themed gifts...", + "searchParams": { + "categories": ["category:0192d4e5-9c0d-7e1f-8a2b-3c4d5e6f7a8b", "category:0192d4e5-ad1e-7f2a-9b3c-4d5e6f7a8b9c"], + "tags": ["science", "educational", "kids"], + "priceRange": [500, 5000], + "ageGroup": "8-12" + }, + "results": ["product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", "product:0192d4e6-3d5f-7b8c-9e0a-1b2c3d4e5f6a", "product:0192d4e6-4e6a-7c9d-8f1b-2c3d4e5f6a7b"], + "timestamp": "2025-05-22T10:00:01Z" + } + ], + "context": { + "intent": "gift_search", + "recipient": "nephew", + "age": 10, + "interests": ["science"], + "budget": null + } +}' +EXPIRE conversation:{sessionId} 1800 + +# Agent tool results cache (avoid re-running expensive searches) +SET agent_cache:{queryHash} '{...results...}' EX 300 + +# Search intent classification (pre-computed embeddings for intent matching) +FT.SEARCH idx:intent_vectors + "*=>[KNN 1 @embedding $query_vec AS score]" + PARAMS 2 query_vec {encoded_query} + DIALECT 2 + +# User preference learning (long-term) +JSON.SET user_preferences:{userId} $ '{ + "pricePreference": "mid-range", + "favoriteCategories": ["category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", "category:0192d4e5-ad1e-7f2a-9b3c-4d5e6f7a8b9c"], + "brandPreferences": ["Samsung", "Sony"], + "avoidCategories": ["category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a"] +}' +``` + +### Agent Tools Definition + +```json +{ + "tools": [ + { + "name": "search_products", + "description": "Search products by keywords, category, and filters", + "parameters": { + "query": "string", + "categories": "string[]", + "tags": "string[]", + "minPrice": "number", + "maxPrice": "number", + "minRating": "number" + } + }, + { + "name": "semantic_search", + "description": "Find products by meaning using vector similarity", + "parameters": { "naturalLanguageQuery": "string", "limit": "number" } + }, + { + "name": "get_product_details", + "description": "Get full details including reviews for a specific product", + "parameters": { "productId": "string" } + }, + { + "name": "check_availability", + "description": "Check if product is in stock and deliverable", + "parameters": { "productId": "string", "postalCode": "string" } + }, + { + "name": "find_similar", + "description": "Find products similar to a given product", + "parameters": { "productId": "string", "limit": "number" } + }, + { + "name": "ask_clarification", + "description": "Ask the user a clarifying question", + "parameters": { "question": "string", "options": "string[]" } + } + ] +} +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/agent/search` | Send natural language query to agent | +| GET | `/api/agent/conversation/:sessionId` | Get conversation history | +| POST | `/api/agent/feedback` | User feedback on results (thumbs up/down) | + +### Request/Response Example + +```json +// Request +{ + "sessionId": "sess_abc123", + "message": "I need a birthday gift for my 10-year-old nephew who likes science" +} + +// Response +{ + "sessionId": "sess_abc123", + "response": "Here are some great science gifts for a 10-year-old! I focused on educational toys and experiment kits:", + "results": [ + { + "productId": "product:0192d4e6-5f7b-7d0e-8a2c-3d4e5f6a7b8c", + "name": "National Geographic Science Kit", + "price": 2499, + "reason": "Highly rated science experiment kit designed for ages 8-12" + }, + { + "productId": "product:0192d4e6-6a8c-7e1f-9b3d-4e5f6a7b8c9d", + "name": "Kids Starter Telescope", + "price": 3999, + "reason": "Perfect for a budding astronomer, includes star chart" + } + ], + "followUp": "Would you like me to filter by a specific budget, or show options in a particular science area (chemistry, astronomy, robotics)?", + "context": { "intent": "gift_search", "refinements_available": true } +} +``` + +### Acceptance Criteria +- Agent correctly interprets natural language queries into search parameters +- Conversation context is maintained across multiple turns +- "Show me cheaper options" correctly references the previous search +- Agent uses multiple tools in sequence when needed +- Results include explanations relevant to the user's stated needs +- Response time is under 3 seconds for the full agent pipeline + +--- + +## General Guidelines + +### Valkey Connection + +All services should connect to Valkey at `localhost:6379` by default, configurable via environment variable: + +``` +VALKEY_URL=redis://localhost:6379 +``` + +### Key Naming Convention + +Since entity IDs already include the domain prefix (`user:uuid`, `product:uuid`), Valkey keys for primary entities **use the ID directly as the key**. For derived/secondary data structures, append qualifiers: + +``` +# Primary entity storage — the key IS the entity ID +user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e → user JSON document +product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f → product JSON document +order:0192d4e8-5e6f-7c8d-8a0b-2c3d4e5f6a7b → order JSON document + +# Derived data — use {purpose}:{entityId} or {purpose}:{qualifier} +cart:user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e → user's cart hash +session:0192d4f0-1a2b-7c3d-8e4f-5a6b7c8d9e0f → session token +trending:global:1h → global trending sorted set +trending:category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b:1h → category trending +ratelimit:user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e:/api/search:1716372600 +``` + +This means `JSON.SET` and `JSON.GET` use the entity ID directly as the key: +``` +JSON.SET product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f $ '{...}' +JSON.GET user:0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e +``` + +Because UUIDv7 is lexicographically sortable by time, you can use `SCAN` with a prefix pattern to find all keys for entities created in a time range, or use `ZRANGEBYLEX` on sorted sets containing prefixed UUIDv7 members. + +### Error Handling + +All API responses should follow this error format: + +```json +{ + "error": "error_code", + "message": "Human-readable description", + "details": {} +} +``` + +### Performance Targets + +| Operation | Target Latency | +|-----------|---------------| +| Key-value read | < 1ms | +| JSON document read | < 5ms | +| Full-text search | < 10ms | +| Vector search (10K docs) | < 10ms | +| Geospatial query | < 5ms | +| Rate limit check | < 2ms | + +Good luck building! 🚀 diff --git a/submissions/valkey-ecommerce-demo/LICENSE b/submissions/valkey-ecommerce-demo/LICENSE new file mode 100644 index 0000000..1921b32 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 opensource-for-valkey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/submissions/valkey-ecommerce-demo/README.md b/submissions/valkey-ecommerce-demo/README.md new file mode 100644 index 0000000..4f144b3 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/README.md @@ -0,0 +1,192 @@ +# Valkey E-Commerce Demo + +An e-commerce platform built with React + Node.js, showcasing **Valkey** as the core data layer across every major e-commerce subsystem. Built with **Breeth AI** for intelligent shopping assistance and personalized memory. + +> Created for the **Build Beyond Limits** hackathon powered by Valkey, hosted by React Hyderabad. + +--- + +## Valkey Features Demonstrated + +| Feature | Valkey Data Structure | Command | Use Case | +|---|---|---|---| +| **Cart Management** | Hash | `HSET / HGETALL / HDEL` | Persistent cart per user, 7-day TTL | +| **Session Storage** | String + TTL | `SET session:{token} EX 86400` | JWT-less auth sessions, 24h expiry | +| **Product Catalog Cache** | JSON | `JSON.SET / JSON.GET` | Product documents + field-level updates | +| **Real-time Inventory** | JSON + Lua | `JSON.NUMINCRBY + EVAL` | Atomic stock reservation, no overselling | +| **Full-Text Search** | Search Module | `FT.CREATE / FT.SEARCH` | Product search with fuzzy match + filters | +| **Trending Products** | Sorted Set | `ZINCRBY / ZREVRANGE` | Time-windowed trending with auto-expire | +| **AI Conversation Cache** | JSON + TTL | `JSON.SET EX 1800` | 30-min session cache for Breeth AI chats | +| **User Affinity** | Sorted Set | `ZINCRBY` | Category preference scoring | +| **Coupon System** | JSON + Set | `JSON.GET / SISMEMBER` | Coupon definitions + per-user usage tracking | +| **Rate Limiting** | String | `INCR / EXPIRE` | Login attempt rate limiting | +| **Autocomplete** | Suggestion Index | `FT.SUGADD / FT.SUGGET` | Live product search suggestions | + +--- + +## Breeth AI Integration + +[Breeth](https://thebreeth.com) provides intent-aware memory for the AI Shopping Assistant: + +- **Long-term user preferences** stored as graph facts (category affinity, browsing patterns) +- **Cross-session memory** — "you bought earbuds last month" style personalization +- **Conversation context** — multi-turn shopping conversations with memory +- **Valkey + Breeth combo**: Valkey caches the short-term conversation (30 min TTL); Breeth stores long-term user graph memory + +--- + +## Tech Stack + +**Frontend:** +- React 18 (Create React App) +- React Router v6 +- Bootstrap 5 + SCSS +- Phosphor Icons + +**Backend:** +- Node.js + Express +- [ioredis](https://github.com/redis/ioredis) — Redis-compatible Valkey client +- [Breeth AI](https://thebreeth.com) — graph memory API + +**Data Layer:** +- [Valkey Bundle](https://github.com/valkey-io/valkey-bundle) — JSON, Search, Bloom, Geo modules + +--- + +## Quick Start + +### Prerequisites +- Node.js v16+ +- Docker + +### 1. Start Valkey + +```bash +docker run -d --name valkey -p 6379:6379 valkey/valkey-bundle:9-alpine +``` + +### 2. Start the backend + +```bash +cd backend +npm install +npm start +``` + +The backend runs on **http://localhost:5000** + +### 3. Seed Valkey with sample data + +```bash +cd backend +node src/seed.js +``` + +This seeds: +- 6 products (phones, laptops, fashion, home, earbuds, sports) +- 10 categories +- 3 coupons: `VALKEY10`, `FLAT500`, `BREETH20` +- 1 demo user: `demo@valkey.io` / `demo1234` + +### 4. Start the frontend + +```bash +cd frontend +npm install +npm start +``` + +The app runs on **http://localhost:3000** + +--- + +## What You'll See + +### Valkey Live Monitor (bottom-right button) +A floating panel showing: +- Real-time key counts per domain (carts, sessions, products, etc.) +- Which Valkey commands are being executed live +- Memory usage +- Feature activation status + +### AI Shopping Assistant (purple robot button) +Powered by **Breeth AI + Valkey**: +- Ask about products, deals, or gift ideas +- Remembers your preferences across sessions (Breeth long-term memory) +- Conversation cached in Valkey JSON with 30-min TTL +- Quick prompts: "Show me trending products", "Best budget smartphones" + +### Cart Page (`/cart`) +- Live cart from Valkey Hash — add/remove/update quantities +- Coupon validation using Valkey JSON + Set +- Shows exactly which Valkey command was executed +- Try coupons: `VALKEY10` (10% off), `FLAT500` (flat ₹500), `BREETH20` (20% off) + +### Valkey Banner (top of page) +Scrolling marquee showing all active Valkey features. + +--- + +## API Reference + +| Method | Endpoint | Valkey Op | Description | +|---|---|---|---| +| GET | `/api/cart?userId=` | `HGETALL` | Fetch user cart | +| POST | `/api/cart/items` | `HSET + EXPIRE` | Add item to cart | +| PATCH | `/api/cart/items/:id` | `HSET` | Update quantity | +| DELETE | `/api/cart/items/:id` | `HDEL` | Remove item | +| POST | `/api/cart/coupon` | `JSON.GET + SISMEMBER` | Apply coupon | +| POST | `/api/auth/login` | `SET session EX 86400` | Login + create session | +| GET | `/api/auth/me` | `GET session + JSON.GET` | Validate session | +| GET | `/api/products` | `JSON.GET (cache)` | List products | +| GET | `/api/products/:id` | `JSON.GET` | Get product | +| GET | `/api/inventory/:id` | `JSON.GET $.inventory` | Real-time stock | +| POST | `/api/inventory/reserve` | `EVAL Lua (atomic)` | Reserve stock | +| GET | `/api/search?q=` | `FT.SEARCH` | Full-text search | +| GET | `/api/search/suggest?q=` | `FT.SUGGET FUZZY` | Autocomplete | +| POST | `/api/ai/chat` | `JSON.SET EX 1800` | AI assistant (Breeth) | +| GET | `/api/ai/trending` | `ZREVRANGE` | Trending products | +| GET | `/api/valkey-stats` | `INFO + KEYS` | Live Valkey stats | + +--- + +## Docker Compose (run everything at once) + +```bash +docker-compose up +``` + +This starts Valkey + the backend together. Then start the frontend separately with `cd frontend && npm start`. + +--- + +## Useful Commands + +```bash +# Open Valkey CLI +docker exec -it valkey valkey-cli + +# Check stored products +valkey-cli JSON.GET product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f + +# Inspect a user's cart +valkey-cli HGETALL cart:user:demo-0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e + +# Check active sessions +valkey-cli KEYS session:* + +# View trending products +valkey-cli ZREVRANGE trending:global:24h 0 9 WITHSCORES + +# Check inventory atomically +valkey-cli JSON.GET product:0192d4e6-6a8c-7e1f-9b3d-4e5f6a7b8c9d $.inventory + +# Search products +valkey-cli FT.SEARCH idx:products "sony" +``` + +--- + +## License + +This project is open source and available for educational and hackathon purposes. diff --git a/submissions/valkey-ecommerce-demo/Valkey-Integrations.md b/submissions/valkey-ecommerce-demo/Valkey-Integrations.md new file mode 100644 index 0000000..40b0a59 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/Valkey-Integrations.md @@ -0,0 +1,79 @@ +# Valkey Integrations + +Valkey is an open source (BSD) high-performance key/value datastore that supports a variety of workloads such as caching, message queues, and can act as a primary database. The project is backed by the Linux Foundation, ensuring it will remain open source forever. + +Valley is looking for attendees to participate in the API challenge choosing one of the following proposed topics, or any other novel use case for an ultra-fast in-memory database. + +| \# | Type | Track | Language | Project | Description | +| :---- | :---- | :---- | :---- | :---- | :---- | +| 1 | New | Web Development | ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | [valkey.io](https://github.com/valkey-io/valkey-io.github.io) | Transform the [valkey.io](http://valkey.io) website from [Zola](https://github.com/getzola/zola) to [Astro](https://github.com/withastro/astro) \+ [React](https://github.com/facebook/react) / [Svelte](https://github.com/sveltejs/svelte). Add interactivity to search anything from anywhere in the site. Include Static pages, Documentation, Blogs, Topics, Commands. | +| 2 | Integration | Web Framework | ![Java](https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/250px-Java_programming_language_logo.svg.png) | [Spring.IO](https://github.com/spring-projects/spring-framework) | Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. A first‑party Valkey integration in the [Spring Framework](https://github.com/spring-projects/spring-framework) delivers native support via Spring Data. It ensures API consistency, shared testing, and unified documentation. Hosting the driver in the Spring repository means it uses the same CI pipelines and security patches as other modules. This cuts external dependency risk and eases version alignment for developers. It also invites deeper community contributions while fitting Spring’s philosophy of seamless ecosystem integration. | +| 3 | Integration | Web Framework | ![PHP](https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/PHP-logo.svg/330px-PHP-logo.svg.png) | [Symfony](https://github.com/symfony/symfony) | Symfony is a PHP framework for web and console applications and a set of reusable PHP components. | +| 4 | Integration | Messaging / Queue Library | ![Python](https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/250px-Python-logo-notext.svg.png?utm_source=commons.wikimedia.org&utm_campaign=index&utm_content=thumbnail) | [Celery](https://github.com/celery/celery) | Distributed Task Queue Integrating Valkey natively into Celery offers seamless compatibility with existing Redis-based workflows while leveraging Valkey’s performance, cost efficiency, and open-source flexibility. A first-party driver simplifies configuration using Valkey as a broker or result backend without custom client code. It ensures optimized serialization, reduces external dependency risks, and aligns with Celery’s ecosystem goals. Developers gain a battle-tested, low-latency solution for distributed task queues, encouraging adoption. Official maintenance guarantees security updates and community-driven enhancements, making Valkey a natural choice for scalable Python task management. | +| 5 | Integration | CMS Framework | ![](image5) | [WordPress](https://github.com/WordPress/wordpress-develop) ![](image6) | WordPress is an open-source content management system (CMS) built with PHP and MySQL, designed to create and manage websites through themes and plugins. Adding a first‑party Valkey integration to [WordPress](https://github.com/WordPress/wordpress-develop) gives the CMS native, high‑performance caching for object storage, session data, and transients. A single, maintained client removes the need for third‑party plugins, cuts compatibility bugs, and aligns with WordPress’s focus on speed and scalability. Developers get a secure, auto‑updated driver that mirrors Redis‑style APIs, easing migration. This tight coupling encourages broader adoption of Valkey for millions of sites, reduces external dependency risk, and leverages Valkey’s open‑source flexibility while keeping the core ecosystem clean and consistent. | +| 6 | Integration | Messaging / Queue Library | ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | [BullMQ](https://github.com/taskforcesh/bullmq) | BullMQ \- Message Queue and Batch processing for NodeJS, Python, Elixir and PHP based on Redis A first‑party Valkey integration for **BullMQ** [npmjs.com](http://npmjs.com) [wdk-docs.github.io](https://wdk-docs.github.io/redis-docs/docs/bull/) gives the queue library native, battle‑tested support for Valkey’s high‑performance in‑memory store [oneuptime.com](https://oneuptime.com/blog/post/2026-01-21-bullmq-connection-options/view). Because BullMQ already relies on Redis commands, switching to Valkey requires *no code changes* the same client code works out‑of‑the‑box [dev.to](https://dev.to/aws/getting-started-with-valkey-using-javascript-3fc9). A first‑party driver hosted in the BullMQ repo ensures API consistency, shared CI testing, and timely security patches. It eliminates the need for custom wrappers or third‑party adapters, lowering maintenance overhead and version‑drift risks. Official maintenance also lets the BullMQ community leverage Valkey‑specific optimizations (e.g., faster Lua scripts, better memory handling), making distributed job processing even more reliable and cost‑effective. | +| 7 | Integration | Web Framework | ![](image7) | [Flask](https://github.com/pallets/flask) | Flask is a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. It began as a simple wrapper around Werkzeug and Jinja, and has become one of the most popular Python web application frameworks. | +| 8 | Integration | Session Library | ![](image8) | [Flask-Session](http://github.com/pallets-eco/flask-session/) | Flask-Session is an extension for Flask that adds support for server-side sessions to your application. **Valkey as a Native Session Backend in Flask-Session** Adding **Valkey** as a native session backend in **Flask-Session** makes migrations from Redis trivial because Valkey is a drop-in replacement for **Redis 7.2**. | +| 9 | New | ORM / ODM Library | ![](image9) | Flask-Valkey né [Flask-Redis](https://github.com/underyx/flask-redis) | A Flask extension for using Redis. Will benefit from first party support for Valkey to use [GLIDE](https://github.com/valkey-io/valkey-glide/) client | +| 10 | New | Proxy / Library | ![](image9) | Open Source S3 Proxy Transparent Cache to Valkey | Create a transparent caching layer using the S3 API Protocol to support Amazon S3 as well as open source alternatives [MinIO](https://github.com/minio/minio), [RustFS](https://github.com/rustfs/rustfs), [Ceph](https://github.com/ceph/ceph), [SeaweedFS](https://github.com/seaweedfs/seaweedfs) Blog: [Open-Source S3 Alternatives| Introduction To MinIO](https://python.plainenglish.io/open-source-s3-options-introduction-to-minio-3e55571eeffb) | +| 11 | Integration | Proxy | ![](image10) | [envoy proxy](https://github.com/envoyproxy/envoy) support for Valkey | Envoy is a CNCF project. Cloud-native high-performance edge/middle/service proxy. Currently supports [Redis protocol](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/redis). Adding Valkey as a first party integration using the GLIDE for Go client will enhance its functionality by doing global cluster SCAN, MSET and MGET commands. | +| 12 | New | JS Runtime | ![](image11] ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | [bun](https://github.com/oven-sh/bun) | Bun is Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one. Add support for Valkey besides already existing [Redis](https://bun.com/docs/runtime/redis) using [GLIDE](https://github.com/valkey-io/valkey-glide/). | +| 13 | Integration | Messaging / Queue Library | ![](image12) | [SideKiq](https://github.com/sidekiq/sidekiq) | Simple, efficient background processing for Ruby, currently supports Redis; will benefit from first party support for Valkey to use [GLIDE](https://github.com/valkey-io/valkey-glide/) client, multiplexing, global SCAN, MGET, MSET and Cluster support. Sidekiq supports Valkey and Dragonfly as Redis alternatives. Redis 7.2.4 is considered to be the canonical implementation. Incompatibilities with that version are considered bugs. | +| 14 | Integration | AI/ML LLM engine | ![](image9) | [vllm](https://github.com/vllm-project/vllm) | A high-throughput and memory-efficient inference and serving engine for LLMs. | +| 15 | Integration | AI/ML Platform | ![](image9) | [langchain](https://github.com/langchain-ai/langchain) | LangChain is a framework for building agents and LLM-powered applications. It helps you chain together interoperable components and third-party integrations to simplify AI application development – all while future-proofing decisions as the underlying technology evolves. Currently supports [Redis](https://github.com/langchain-ai/langchain-redis), and AWS [In-MemoryDB](https://github.com/langchain-ai/langchain-aws/tree/main/libs/aws/langchain_aws/vectorstores/inmemorydb). Will benefit of supporting [Valkey Vector Similarity Search](https://github.com/valkey-io/valkey-search) for higher throughput, lower latency, and continuous OSS support from Linux Foundation. | +| 16 | Integration | Web Framework | ![](image9) | [fastapi](https://github.com/fastapi/fastapi) | FastAPI framework, high performance, easy to learn, fast to code, ready for production. | +| 17 | New | Library | ![](image9) | [fastapi-cache](https://github.com/long2ice/fastapi-cache) | fastapi-cache is a tool to cache fastapi response and function result, with backends support redis and memcached, missing support for Valkey and use [GLIDE](https://github.com/valkey-io/valkey-glide/) for Python. | +| 18 | Integration | Web Framework | ![](image9) | [starlette](https://github.com/Kludex/starlette) | Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python. Currently support Redis through [starlette-sessions](https://pypi.org/project/starlette-sessions/) | +| 19 | Integration | PaaS Platform | ![](image5) | [Coolify](https://github.com/coollabsio/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. Currently support [Redis](https://coolify.io/docs/databases/redis) and [Dragonfly](https://coolify.io/docs/databases/dragonfly), missing support for Valkey. | +| 20 | Integration | AI/ML Framework | ![](image9) | [feast](https://github.com/feast-dev/feast) | Feast (Feature Store) is an open source feature store for machine learning. Feast is the fastest path to manage existing infrastructure to productionize analytic data for model training and online inference. Currently supports [Redis](https://docs.feast.dev/reference/online-stores/redis), missing Valkey as Online Datastores. | +| 21 | Integration | AI/ML Framework | ![](image9) | [feast](https://github.com/feast-dev/feast) | Missing support for [Iceberg](https://github.com/apache/iceberg) on Offline Datastores | +| 22 | Integration | Web Framework | ![](image5) | [laravel](https://github.com/laravel/laravel) | Laravel is a web application framework with expressive, elegant syntax. We’ve already laid the foundation for your next big idea — freeing you to create without sweating the small things. Currently has support for Redis, will benefit from first party integration of Valkey using [GLIDE for PHP](https://github.com/valkey-io/valkey-glide-php). | +| 23 | New | UI Tool | ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | Valkey UI | Currently there's a couple of options to see your Valkey Data. [Valkey Admin](https://github.com/valkey-io/valkey-admin) is a web-based administration tool for Valkey clusters. It provides an intuitive interface to monitor, manage, and interact with your Valkey instances, offering features like real-time metrics and key management. [BetterDB](https://github.com/BetterDB-inc) provides the tooling ecosystem for Valkey, the high-performance Redis fork backed by AWS, Google, and Oracle. With a founding CTO from Redis, we're capturing the massive installed base as cloud providers drive the shift to Valkey's superior performance and permissive licensing. A leaner tool using [Svelte](https://github.com/sveltejs/svelte) will be the best given, Valkey's low memory footprint it aligns better with a compiled project rather than a Virtual DOM like React. | +| 24 | New | Security | ![](image9) | [authentik](https://github.com/goauthentik/authentik) | authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters. Blog: [We removed Redis](https://goauthentik.io/blog/2025-11-13-we-removed-redis/) since didn't align with OSS principles. | +| 25 | Integration | Ops | ![](image9) | [airflow](https://github.com/apache/airflow) | Apache Airflow \- A platform to programmatically author, schedule, and monitor workflows. Currently supports [Redis](https://airflow.apache.org/docs/apache-airflow-providers-redis/stable/index.html) | +| 26 | Integration | Websockets | ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | [socket.io](https://github.com/socketio/socket.io/) | Bidirectional and low-latency communication for every platform. Realtime application framework (Node.JS server). Currently support [Redis](https://socket.io/docs/v4/redis-adapter/) | +| 27 | New | Messaging / Queue Platform | ![Java](https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/250px-Java_programming_language_logo.svg.png) | Valkey Kafka Connector né [Redis Kafka Connector](https://www.confluent.io/hub/redis/redis-kafka-connect) | [Apache Kafka](https://kafka.apache.org/) is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications. Adding Valkey as a sink will benefit real-time use cases. As explained in the Blog: [Low Latency Real-Time Cache Updates with Amazon ElastiCache for Redis and Confluent Cloud Kafka](https://aws.amazon.com/blogs/apn/low-latency-real-time-cache-updates-with-amazon-elasticache-for-redis-and-confluent-cloud-kafka/). | +| 28 | Integration | Distributed SQL | ![Java](https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/250px-Java_programming_language_logo.svg.png) | [trino](https://github.com/trinodb/trino) | Trino, the distributed SQL query engine for big data, formerly known as PrestoSQL ([https://trino.io](https://trino.io)) Current support for [Redis](https://trino.io/docs/current/connector/redis.html). | +| 29 | Integration | e-commerce | ![](image5) | [OpenCart](https://github.com/opencart/opencart) | A free shopping cart system. OpenCart is an open source PHP-based online e-commerce solution. | +| 30 | Integration | e-commerce | ![](image5) | [PrestaShop](https://github.com/PrestaShop/PrestaShop) | PrestaShop is the universal open-source software platform to build your e-commerce solution. | +| 31 | Integration | e-commerce | ![](image5) | [Magento2](https://github.com/magento/magento2) | Magento Open Source software delivers basic eCommerce capabilities to build a unique online store from the ground up. | +| 32 | New | AI/ML Agentic | ![](image9) | [Strands Agents](https://github.com/strands-agents) | A model-driven, open source SDK for creating AI agents—from simple conversational assistants to complex autonomous workflows. Python and TypeScript. Apache 2.0. | +| 33 | Integration | AI/ML Agentic | ![](image9) | [LlamaIndex](https://github.com/run-llama/llama_index) | [LlamaIndex](https://developers.llamaindex.ai/) is the leading framework for building LLM-powered agents over your data. | +| 34 | Integration | AI/ML Agentic | ![](image9) | [CrewAI](https://github.com/crewAIInc/crewAI) | Framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, [CrewAI](https://crewai.com/) empowers agents to work together seamlessly, tackling complex tasks. | +| 35 | New | AI/ML Agentic | ![JavaScript](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/JavaScript-logo.png/250px-JavaScript-logo.png) | [OpenClaw](https://github.com/openclaw/openclaw) | [OpenClaw](https://openclaw.ai/) is a personal AI assistant you run on your own devices. It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant. Valkey can help for distributed semantic cache. | +| 36 | New | AI/ML Agentic | ![](image10) | [PicoClaw](https://github.com/sipeed/picoclaw) | PicoClaw is an ultra-lightweight personal AI Assistant inspired by nanobot, refactored from the ground up in Go through a self-bootstrapping process, where the AI agent itself drove the entire architectural migration and code optimization. Valkey can help for distributed semantic cache. | +| 37 | New | AI/ML Agentic | ![](image9) | [OpenViking](https://github.com/volcengine/OpenViking) | [OpenViking](https://openviking.ai/) is an open-source context database designed specifically for AI Agents. OpenViking unifies the management of context (memory, resources, and skills) that Agents need through a file system paradigm, enabling hierarchical context delivery and self-evolving. | +| 38 | New | AI/ML Deep Research framework | ![](image9) | [DeerFlow](https://github.com/bytedance/deer-flow) | DeerFlow is a community-driven Deep Research framework, combining language models with tools like web search, crawling, and Python execution, while contributing back to the open-source community. | +| 39 | New | AI/ML RAG engine | ![](image9) | [RAGFlow](https://github.com/infiniflow/ragflow) | [RAGFlow](https://ragflow.io/) is a leading open-source Retrieval-Augmented Generation (RAG) engine that fuses cutting-edge RAG with Agent capabilities to create a superior context layer for LLMs | +| 40 | Integration | AI/ML engine | ![](image9) | [Ray](https://github.com/ray-project/ray) | [Ray](https://ray.io/) is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads. | + +Samples + +- [Bedrock Example with Strands](https://github.com/aws-samples/amazon-elasticache-samples/blob/main/navigator/backend/agent.py) +- [https://github.com/valkey-io/spring-data-valkey](https://github.com/valkey-io/spring-data-valkey) +- [https://github.com/django-commons/django-valkey](https://github.com/django-commons/django-valkey) + +Other Ideas + +- RATE LIMITER +- CONSISTENT HASHING +- URL SHORTENER +- NOTIFICATION SYSTEM +- NEWS FEED SYSTEM +- CHAT SYSTEM +- SEARCH AUTOCOMPLETE SYSTEM +- Proximity Service +- Nearby Friends +- Online Maps +- Distributed Message Queue +- Metrics Monitoring and Alerting System +- Ad Click Event Aggregation +- Hotel Reservation System +- Distributed Email Service +- S3-like Object Storage +- Real-time Gaming Leaderboard +- Payment System +- Digital Wallet +- Stock Exchange +- Matchmaking system +- Transparent Caching based on Write Ahead Log using [Debezium](https://github.com/debezium/debezium) diff --git a/submissions/valkey-ecommerce-demo/backend/.env b/submissions/valkey-ecommerce-demo/backend/.env new file mode 100644 index 0000000..cd51370 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/.env @@ -0,0 +1,4 @@ +VALKEY_URL=redis://localhost:6379 +PORT=5000 +BREETH_API_KEY=ck_live_YhQWb-ZjIQxfs6AzDezC3zaPMPw4xJ8yRDkjIz7d16A +FRONTEND_URL=http://localhost:3000 diff --git a/submissions/valkey-ecommerce-demo/backend/Dockerfile b/submissions/valkey-ecommerce-demo/backend/Dockerfile new file mode 100644 index 0000000..8686c56 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 5000 +CMD ["node", "server.js"] diff --git a/submissions/valkey-ecommerce-demo/backend/package-lock.json b/submissions/valkey-ecommerce-demo/backend/package-lock.json new file mode 100644 index 0000000..f62862b --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/package-lock.json @@ -0,0 +1,1549 @@ +{ + "name": "valkey-ecommerce-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "valkey-ecommerce-backend", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.19.2", + "ioredis": "^5.3.2", + "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/submissions/valkey-ecommerce-demo/backend/package.json b/submissions/valkey-ecommerce-demo/backend/package.json new file mode 100644 index 0000000..593bcdf --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "valkey-ecommerce-backend", + "version": "1.0.0", + "description": "Valkey-powered e-commerce backend with Breeth AI integration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "seed": "node src/seed.js" + }, + "dependencies": { + "axios": "^1.7.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.6.1", + "express": "^4.19.2", + "ioredis": "^5.3.2", + "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.4" + } +} diff --git a/submissions/valkey-ecommerce-demo/backend/server.js b/submissions/valkey-ecommerce-demo/backend/server.js new file mode 100644 index 0000000..6b7101e --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/server.js @@ -0,0 +1,61 @@ +require("dotenv").config(); +const express = require("express"); +const cors = require("cors"); +const { valkey } = require("./src/valkey"); +const { createSearchIndex } = require("./src/routes/search"); + +const app = express(); +const PORT = process.env.PORT || 5000; + +app.use(cors({ origin: process.env.FRONTEND_URL || "http://localhost:3000", credentials: true })); +app.use(express.json()); + +// Request logger +app.use((req, _res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + next(); +}); + +// Routes +app.use("/api/auth", require("./src/routes/auth")); +app.use("/api/cart", require("./src/routes/cart")); +app.use("/api/products", require("./src/routes/products")); +app.use("/api/inventory", require("./src/routes/inventory")); +app.use("/api/search", require("./src/routes/search").router); +app.use("/api/ai", require("./src/routes/ai")); +app.use("/api/wishlist", require("./src/routes/wishlist")); +app.use("/api/valkey-stats", require("./src/routes/stats")); +app.use("/api/comparison", require("./src/routes/comparison")); +app.use("/api/analytics", require("./src/routes/analytics")); +app.use("/api/admin", require("./src/routes/admin")); +app.use("/api/ads", require("./src/routes/ads")); + +// Health +app.get("/health", (_req, res) => res.json({ status: "ok", timestamp: new Date().toISOString() })); + +async function start() { + await valkey.connect(); + // Initialize Valkey Search index + await createSearchIndex(); + app.listen(PORT, () => { + console.log(`\n[Server] Valkey E-Commerce Backend running on http://localhost:${PORT}`); + console.log("[Server] Valkey URL:", process.env.VALKEY_URL || "redis://localhost:6379"); + console.log("[Server] Breeth AI: connected"); + console.log("\n[Valkey Features Active]"); + console.log(" • Cart Management → HSET/HGETALL (Valkey Hash)"); + console.log(" • Session Storage → SET/GET EX 86400 (Valkey String)"); + console.log(" • Product Catalog → JSON.SET/GET (Valkey JSON)"); + console.log(" • Product Cache → GET/SETEX (Valkey String cache)"); + console.log(" • Real-time Inventory→ JSON.NUMINCRBY + Lua (atomic)"); + console.log(" • Full-Text Search → FT.SEARCH (Valkey Search Module)"); + console.log(" • AI Conv. Cache → JSON.SET EX 1800 (Valkey JSON)"); + console.log(" • Trending Products → ZINCRBY/ZREVRANGE (Valkey Sorted Set)"); + console.log(" • User Affinity → ZINCRBY (Valkey Sorted Set)"); + console.log(" • Rate Limiting → INCR/EXPIRE (Valkey String counter)\n"); + }); +} + +start().catch((err) => { + console.error("[Server] Fatal error:", err); + process.exit(1); +}); diff --git a/submissions/valkey-ecommerce-demo/backend/src/breeth.js b/submissions/valkey-ecommerce-demo/backend/src/breeth.js new file mode 100644 index 0000000..dfee082 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/breeth.js @@ -0,0 +1,64 @@ +const axios = require("axios"); + +const BREETH_API_KEY = process.env.BREETH_API_KEY || "ck_live_YhQWb-ZjIQxfs6AzDezC3zaPMPw4xJ8yRDkjIz7d16A"; +const BREETH_BASE = "https://api.thebreeth.com/v1"; + +const breethClient = axios.create({ + baseURL: BREETH_BASE, + headers: { + Authorization: `Bearer ${BREETH_API_KEY}`, + "Content-Type": "application/json", + }, + timeout: 8000, +}); + +async function recordFact(subject, predicate, object, metadata = {}) { + try { + const res = await breethClient.post("/facts", { + subject, + predicate, + object, + metadata, + }); + return res.data; + } catch (err) { + console.error("[Breeth] recordFact error:", err.message); + return null; + } +} + +async function searchGraph(query, options = {}) { + try { + const res = await breethClient.get("/graph/search", { + params: { q: query, ...options }, + }); + return res.data; + } catch (err) { + console.error("[Breeth] searchGraph error:", err.message); + return { results: [] }; + } +} + +async function getEntityView(entityId) { + try { + const res = await breethClient.get(`/graph/entity/${encodeURIComponent(entityId)}`); + return res.data; + } catch (err) { + console.error("[Breeth] getEntityView error:", err.message); + return null; + } +} + +async function getUserPreferences(userId) { + try { + const res = await breethClient.get("/graph/search", { + params: { q: userId, limit: 20 }, + }); + return res.data?.results || []; + } catch (err) { + console.error("[Breeth] getUserPreferences error:", err.message); + return []; + } +} + +module.exports = { recordFact, searchGraph, getEntityView, getUserPreferences }; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/admin.js b/submissions/valkey-ecommerce-demo/backend/src/routes/admin.js new file mode 100644 index 0000000..694c1c1 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/admin.js @@ -0,0 +1,458 @@ +/** + * Admin routes – protected by X-Admin-Token header or admin session + * + * Middleware: X-Admin-Token: admin-valkey-demo OR session token with role="admin" + * + * Valkey commands: + * KEYS user:* – list all users + * JSON.GET {userId} – fetch user profiles + * JSON.SET $.inventory – update inventory levels + * KEYS cart:* – list all carts (order snapshots) + * HGETALL cart:* – fetch cart contents + * DEL user:* keys – delete user data + * JSON.SET new product – add product + * FT.SUGADD – add product to autocomplete + * INFO / DBSIZE – system stats + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, getStats } = require("../valkey"); +const { v7: uuidv7 } = require("uuid"); + +const ADMIN_TOKEN = "admin-valkey-demo"; + +// Middleware: check X-Admin-Token header or session with role="admin" +async function requireAdmin(req, res, next) { + const headerToken = req.headers["x-admin-token"]; + if (headerToken === ADMIN_TOKEN) return next(); + + // Check session token + const bearerToken = req.headers.authorization?.replace("Bearer ", ""); + if (bearerToken) { + try { + const userId = await valkey.get(`session:${bearerToken}`); + if (userId) { + const raw = await valkey.call("JSON.GET", userId); + if (raw) { + const user = JSON.parse(raw); + if (user.role === "admin" || user.role === "manager") { + req.adminUser = user; + return next(); + } + } + } + } catch (_) {} + } + + res.status(401).json({ + error: "unauthorized", + message: "Admin access required. Provide X-Admin-Token: admin-valkey-demo header or an admin session token.", + }); +} + +router.use(requireAdmin); + +// GET /api/admin/users +router.get("/users", async (req, res) => { + try { + const userKeys = await valkey.keys("user:*"); + const users = []; + + for (const uk of userKeys) { + try { + const raw = await valkey.call("JSON.GET", uk); + if (raw) { + const user = JSON.parse(raw); + const { passwordHash, ...safeUser } = user; + users.push(safeUser); + } + } catch (_) {} + } + + res.json({ + count: users.length, + users, + valkeyOp: `KEYS user:* → JSON.GET each (${userKeys.length} users)`, + storedIn: "Valkey JSON (user profiles)", + }); + } catch (err) { + res.status(500).json({ error: "admin_users_error", message: err.message }); + } +}); + +// GET /api/admin/inventory +router.get("/inventory", async (req, res) => { + try { + const productKeys = await valkey.keys("product:*"); + const products = []; + const lowStockAlerts = []; + + for (const pk of productKeys) { + try { + const raw = await valkey.call("JSON.GET", pk); + if (raw) { + const product = JSON.parse(raw); + const inv = product.inventory || {}; + const available = (inv.quantity || 0) - (inv.reserved || 0); + + const entry = { + productId: pk, + name: product.name, + brand: product.brand, + sku: product.sku, + categoryId: product.categoryId, + price: product.price, + inventory: inv, + available, + status: product.status, + }; + products.push(entry); + + if (available < 10) { + lowStockAlerts.push({ + ...entry, + alertLevel: available <= 0 ? "out_of_stock" : available < 5 ? "critical" : "low", + }); + } + } + } catch (_) {} + } + + res.json({ + totalProducts: products.length, + lowStockCount: lowStockAlerts.length, + lowStockAlerts, + products, + valkeyOp: `KEYS product:* → JSON.GET each (${productKeys.length} products)`, + storedIn: "Valkey JSON (product documents)", + }); + } catch (err) { + res.status(500).json({ error: "admin_inventory_error", message: err.message }); + } +}); + +// PATCH /api/admin/inventory/:productId +router.patch("/inventory/:productId", async (req, res) => { + const { productId } = req.params; + const { quantity, reserved } = req.body; + + if (quantity === undefined && reserved === undefined) { + return res.status(400).json({ error: "quantity or reserved is required" }); + } + + const key = `product:${productId}`; + try { + const exists = await valkey.call("JSON.GET", key); + if (!exists) return res.status(404).json({ error: "Product not found" }); + + const valkeyOps = []; + + if (quantity !== undefined) { + await valkey.call("JSON.SET", key, "$.inventory.quantity", JSON.stringify(Number(quantity))); + valkeyOps.push(`JSON.SET ${key} $.inventory.quantity ${quantity}`); + } + + if (reserved !== undefined) { + await valkey.call("JSON.SET", key, "$.inventory.reserved", JSON.stringify(Number(reserved))); + valkeyOps.push(`JSON.SET ${key} $.inventory.reserved ${reserved}`); + } + + // Update updatedAt + await valkey.call("JSON.SET", key, "$.updatedAt", JSON.stringify(new Date().toISOString())); + valkeyOps.push(`JSON.SET ${key} $.updatedAt`); + + const updatedRaw = await valkey.call("JSON.GET", key); + const updated = JSON.parse(updatedRaw); + + res.json({ + success: true, + productId: key, + inventory: updated.inventory, + available: updated.inventory.quantity - (updated.inventory.reserved || 0), + valkeyOp: valkeyOps.join(" → "), + storedIn: "Valkey JSON (product document)", + }); + } catch (err) { + res.status(500).json({ error: "admin_inventory_update_error", message: err.message }); + } +}); + +// GET /api/admin/orders — treat carts as order snapshots +router.get("/orders", async (req, res) => { + try { + const cartKeys = await valkey.keys("cart:*"); + const orders = []; + + for (const ck of cartKeys) { + try { + const cartData = await valkey.hgetall(ck); + if (cartData && Object.keys(cartData).length > 0) { + const items = []; + let orderTotal = 0; + + for (const [productId, val] of Object.entries(cartData)) { + try { + const item = JSON.parse(val); + items.push({ productId, ...item }); + if (item.price && item.quantity) { + orderTotal += item.price * item.quantity; + } + } catch (_) {} + } + + orders.push({ + cartKey: ck, + userId: ck.replace("cart:", ""), + itemCount: items.length, + items, + estimatedTotal: orderTotal, + currency: "INR", + status: "active_cart", + }); + } + } catch (_) {} + } + + res.json({ + totalOrders: orders.length, + orders, + valkeyOp: `KEYS cart:* → HGETALL each (${cartKeys.length} carts)`, + storedIn: "Valkey Hash (cart snapshots)", + }); + } catch (err) { + res.status(500).json({ error: "admin_orders_error", message: err.message }); + } +}); + +// DELETE /api/admin/users/:userId +router.delete("/users/:userId", async (req, res) => { + const { userId } = req.params; + const userKey = userId.startsWith("user:") ? userId : `user:${userId}`; + + try { + // Get user details first to also delete email index + const raw = await valkey.call("JSON.GET", userKey); + if (!raw) return res.status(404).json({ error: "User not found" }); + + const user = JSON.parse(raw); + const deletedKeys = []; + + // Delete user JSON + await valkey.del(userKey); + deletedKeys.push(userKey); + + // Delete email index + if (user.email) { + await valkey.del(`email:${user.email}`); + deletedKeys.push(`email:${user.email}`); + } + + // Delete session keys (scan for sessions belonging to this user) + try { + const sessionKeys = await valkey.keys("session:*"); + for (const sk of sessionKeys) { + const sessionUserId = await valkey.get(sk); + if (sessionUserId === userKey) { + await valkey.del(sk); + deletedKeys.push(sk); + } + } + } catch (_) {} + + // Delete cart + try { + await valkey.del(`cart:${userKey}`); + deletedKeys.push(`cart:${userKey}`); + } catch (_) {} + + // Delete wishlist + try { + await valkey.del(`wishlist:${userKey}`); + deletedKeys.push(`wishlist:${userKey}`); + } catch (_) {} + + // Delete comparison set + try { + await valkey.del(`compare:${userKey}`); + deletedKeys.push(`compare:${userKey}`); + } catch (_) {} + + res.json({ + success: true, + deletedUserId: userKey, + deletedKeys, + valkeyOp: `DEL ${deletedKeys.join(", DEL ")}`, + storedIn: "Valkey JSON, String, Hash, Set", + }); + } catch (err) { + res.status(500).json({ error: "admin_delete_user_error", message: err.message }); + } +}); + +// POST /api/admin/products +router.post("/products", async (req, res) => { + const { name, sku, description, shortDescription, categoryId, vendorId, brand, price, images, attributes, tags, inventory, status } = req.body; + + if (!name || !price || !categoryId) { + return res.status(400).json({ error: "name, price, and categoryId are required" }); + } + + try { + const productId = `product:${uuidv7()}`; + const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); + const now = new Date().toISOString(); + + const product = { + id: productId, + sku: sku || `PROD-${Date.now()}`, + name, + slug, + description: description || "", + shortDescription: shortDescription || "", + categoryId, + vendorId: vendorId || "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: brand || "", + price: typeof price === "object" ? price : { amount: price, currency: "INR" }, + images: images || [{ url: "/assets/images/thumbs/product-img1.png", alt: name, isPrimary: true }], + attributes: attributes || {}, + tags: tags || [], + inventory: inventory || { quantity: 0, reserved: 0, warehouse: "HYD-WH-01" }, + ratings: { average: 0, count: 0 }, + status: status || "active", + createdAt: now, + updatedAt: now, + }; + + // Store product in Valkey JSON + await valkey.call("JSON.SET", productId, "$", JSON.stringify(product)); + + // Add to category index + if (categoryId) { + await valkey.zadd(`category_products:${categoryId}`, Date.now(), productId); + } + + // Add to price index + if (product.price?.amount) { + await valkey.zadd("price_index", product.price.amount, productId); + } + + // Add to brand index + if (brand) { + await valkey.sadd(`brand_products:${brand.toLowerCase()}`, productId); + } + + // Add to autocomplete + try { + await valkey.call("FT.SUGADD", "autocomplete", name, 100); + } catch (_) {} + + res.status(201).json({ + success: true, + product, + valkeyOp: `JSON.SET ${productId} → ZADD category_products → ZADD price_index → SADD brand_products → FT.SUGADD autocomplete`, + storedIn: "Valkey JSON + Sorted Set + Set + Suggestion Index", + }); + } catch (err) { + res.status(500).json({ error: "admin_create_product_error", message: err.message }); + } +}); + +// GET /api/admin/stats +router.get("/stats", async (req, res) => { + try { + const valkeyOps = []; + + // Key counts + const [productKeys, userKeys, categoryKeys, sessionKeys, cartKeys, wishlistKeys, compareKeys, couponKeys] = await Promise.all([ + valkey.keys("product:*"), + valkey.keys("user:*"), + valkey.keys("category:*"), + valkey.keys("session:*"), + valkey.keys("cart:*"), + valkey.keys("wishlist:*"), + valkey.keys("compare:*"), + valkey.keys("coupon:*"), + ]); + valkeyOps.push("KEYS product:*, user:*, category:*, session:*, cart:*, wishlist:*, compare:*, coupon:*"); + + // DB size + let dbSize = 0; + try { + dbSize = await valkey.dbsize(); + valkeyOps.push("DBSIZE"); + } catch (_) {} + + // Memory info + let memoryInfo = {}; + try { + const info = await valkey.info("memory"); + valkeyOps.push("INFO memory"); + const lines = info.split("\r\n"); + for (const line of lines) { + if (line.startsWith("used_memory_human:")) memoryInfo.usedMemory = line.split(":")[1].trim(); + if (line.startsWith("used_memory_peak_human:")) memoryInfo.peakMemory = line.split(":")[1].trim(); + if (line.startsWith("mem_fragmentation_ratio:")) memoryInfo.fragmentationRatio = line.split(":")[1].trim(); + } + } catch (e) { + valkeyOps.push("INFO memory (error: " + e.message + ")"); + } + + // Feature usage stats + const featureUsage = getStats(); + valkeyOps.push("getStats() from valkey.js usage counters"); + + // Trending set sizes + let trendingCount = 0; + try { + trendingCount = await valkey.zcard("trending:global:24h"); + valkeyOps.push("ZCARD trending:global:24h"); + } catch (_) {} + + res.json({ + timestamp: new Date().toISOString(), + keyCounts: { + products: productKeys.length, + users: userKeys.length, + categories: categoryKeys.length, + sessions: sessionKeys.length, + carts: cartKeys.length, + wishlists: wishlistKeys.length, + comparisons: compareKeys.length, + coupons: couponKeys.length, + total: dbSize, + }, + memory: memoryInfo, + featureUsage, + trending: { + productsInTrendingSet: trendingCount, + }, + valkeyOps, + storedIn: "Valkey JSON, String, Hash, Set, Sorted Set", + }); + } catch (err) { + res.status(500).json({ error: "admin_stats_error", message: err.message }); + } +}); + +// POST /api/admin/reseed +router.post("/reseed", async (req, res) => { + const { fork } = require("child_process"); + const path = require("path"); + try { + const seedPath = path.join(__dirname, "../seed.js"); + const child = fork(seedPath); + + child.on("close", (code) => { + if (code === 0) { + res.json({ status: "success", message: "Database reseeded successfully." }); + } else { + res.status(500).json({ error: "seed_error", message: `Seeding process exited with code ${code}` }); + } + }); + } catch (err) { + res.status(500).json({ error: "reseed_failed", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/ads.js b/submissions/valkey-ecommerce-demo/backend/src/routes/ads.js new file mode 100644 index 0000000..1de0f95 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/ads.js @@ -0,0 +1,182 @@ +/** + * Targeted Ads – Valkey Sorted Set (user affinity) + Search + * + * Valkey commands: + * ZREVRANGE user_affinity:{userId} 0 2 WITHSCORES – get top 3 interest categories + * FT.SEARCH @categoryId:{catId} – find products per category + * ZREVRANGE trending:global:24h 0 5 – fallback trending products + * JSON.GET {productId} – fetch product details + */ + +const express = require("express"); +const router = express.Router(); +const { valkey } = require("../valkey"); + +// GET /api/ads/targeted?userId= +router.get("/targeted", async (req, res) => { + const { userId } = req.query; + + const valkeyOps = []; + const ads = []; + + try { + let usedAffinity = false; + + if (userId) { + // Get top 3 categories from user affinity sorted set + const affinityKey = `user_affinity:${userId}`; + const affinityRaw = await valkey.zrevrange(affinityKey, 0, 2, "WITHSCORES"); + valkeyOps.push(`ZREVRANGE ${affinityKey} 0 2 WITHSCORES`); + + const affinities = []; + for (let i = 0; i < affinityRaw.length; i += 2) { + affinities.push({ categoryId: affinityRaw[i], score: parseFloat(affinityRaw[i + 1]) }); + } + + if (affinities.length > 0) { + usedAffinity = true; + + for (const affinity of affinities) { + try { + // Escape the categoryId for FT.SEARCH TAG filter + const escapedCat = affinity.categoryId.replace(/:/g, "\\:"); + const searchResult = await valkey.call( + "FT.SEARCH", "idx:products", + `@categoryId:{${escapedCat}}`, + "LIMIT", "0", "3", + "RETURN", "5", "name", "price", "brand", "$.images[0].url", "$.images[0].alt" + ); + valkeyOps.push(`FT.SEARCH idx:products @categoryId:{${affinity.categoryId}} LIMIT 0 3`); + + const total = searchResult[0]; + for (let i = 1; i < searchResult.length; i += 2) { + const pid = searchResult[i]; + const fields = searchResult[i + 1]; + const obj = { id: pid }; + for (let j = 0; j < fields.length; j += 2) { + let key = fields[j]; + if (key === "$.images[0].url") key = "image"; + if (key === "$.images[0].alt") key = "imageAlt"; + obj[key] = fields[j + 1]; + } + ads.push({ + productId: pid, + name: obj.name || pid, + price: obj.price ? Number(obj.price) : null, + brand: obj.brand || null, + image: obj.image || "/assets/images/thumbs/product-img1.png", + adType: "targeted", + targetReason: "Based on your browsing", + categoryId: affinity.categoryId, + affinityScore: affinity.score, + }); + } + } catch (e) { + valkeyOps.push(`FT.SEARCH for ${affinity.categoryId} (error: ${e.message})`); + } + } + } + } + + // Fallback: use trending products if no affinity data or userId not provided + if (ads.length === 0) { + const trendingRaw = await valkey.zrevrange("trending:global:24h", 0, 5, "WITHSCORES"); + valkeyOps.push("ZREVRANGE trending:global:24h 0 5 WITHSCORES (fallback — no affinity data)"); + + for (let i = 0; i < trendingRaw.length; i += 2) { + const pid = trendingRaw[i]; + const score = parseFloat(trendingRaw[i + 1]); + try { + const raw = await valkey.call("JSON.GET", pid); + valkeyOps.push(`JSON.GET ${pid}`); + if (raw) { + const product = JSON.parse(raw); + ads.push({ + productId: pid, + name: product.name, + price: product.price?.amount || null, + brand: product.brand || null, + image: product.images?.[0]?.url || "/assets/images/thumbs/product-img1.png", + adType: "trending", + targetReason: "Trending now", + trendScore: score, + }); + } + } catch (_) {} + } + } + + // Deduplicate by productId + const seen = new Set(); + const uniqueAds = ads.filter((ad) => { + if (seen.has(ad.productId)) return false; + seen.add(ad.productId); + return true; + }).slice(0, 6); + + res.json({ + userId: userId || "guest", + adType: ads.length > 0 && ads[0].adType === "targeted" ? "targeted" : "trending", + count: uniqueAds.length, + ads: uniqueAds, + valkeyOps, + storedIn: "Valkey Sorted Set (user_affinity + trending) + Search Index", + }); + } catch (err) { + res.status(500).json({ error: "ads_targeted_error", message: err.message }); + } +}); + +// GET /api/ads/banner +router.get("/banner", async (req, res) => { + const banners = [ + { + id: "banner-001", + title: "Save 10% with VALKEY10", + subtitle: "Use coupon code VALKEY10 on orders above ₹5,000", + couponCode: "VALKEY10", + discount: "10% OFF", + backgroundColor: "#7C3AED", + textColor: "#FFFFFF", + ctaText: "Shop Now", + ctaUrl: "/shop", + imageUrl: "/assets/images/banners/valkey-banner.png", + validUntil: "2026-12-31", + adType: "coupon", + priority: 1, + }, + { + id: "banner-002", + title: "New Arrivals — Apple Collection", + subtitle: "iPhone 15 Pro, MacBook Air M3 & Apple Watch Series 9 now in stock", + backgroundColor: "#1D1D1F", + textColor: "#F5F5F7", + ctaText: "Explore New Arrivals", + ctaUrl: "/shop?brand=Apple", + imageUrl: "/assets/images/banners/new-arrivals.png", + adType: "new_arrivals", + priority: 2, + }, + { + id: "banner-003", + title: "Trending Deals This Week", + subtitle: "Up to 40% off on Electronics, Fashion & Home Appliances", + backgroundColor: "#059669", + textColor: "#FFFFFF", + ctaText: "See Trending Deals", + ctaUrl: "/shop?sort=trending", + imageUrl: "/assets/images/banners/trending-deals.png", + adType: "trending_deals", + priority: 3, + }, + ]; + + res.json({ + count: banners.length, + banners, + valkeyOp: "Static promotional banners (no Valkey read required)", + storedIn: "Hardcoded promotional content", + }); +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/ai.js b/submissions/valkey-ecommerce-demo/backend/src/routes/ai.js new file mode 100644 index 0000000..5520f8f --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/ai.js @@ -0,0 +1,368 @@ +/** + * AI Shopping Assistant – Breeth AI + Valkey Session Cache + * + * Breeth stores: + * - User shopping preferences as graph facts + * - Conversation context between turns + * - Category affinity from browsing behavior + * + * Valkey stores: + * - Conversation session cache (JSON, TTL 30 min) + * - Agent tool result cache (TTL 5 min) + * - User behavior events for real-time recommendations + */ + +const express = require("express"); +const router = express.Router(); +const { v7: uuidv7 } = require("uuid"); +const { valkey, track } = require("../valkey"); +const { recordFact, searchGraph, getUserPreferences } = require("../breeth"); + +const CONVO_TTL = 1800; // 30 min conversation cache in Valkey + +// POST /api/ai/chat – send message to AI shopping assistant +router.post("/chat", async (req, res) => { + const { sessionId, message, userId = "guest:demo" } = req.body; + if (!message) return res.status(400).json({ error: "message required" }); + + const sid = sessionId || `session:${uuidv7()}`; + const convoKey = `conversation:${sid}`; + + try { + // Load conversation history from Valkey + const raw = await valkey.call("JSON.GET", convoKey); + track("aiOps"); + + const conversation = raw ? JSON.parse(raw) : { sessionId: sid, userId, turns: [], context: {} }; + + // Add user message + conversation.turns.push({ role: "user", content: message, timestamp: new Date().toISOString() }); + + // Get user preferences from Breeth + const preferences = await getUserPreferences(userId); + + // Build AI response (intelligent product-aware responses) + const aiResponse = await generateAIResponse(message, conversation, preferences); + + // Add AI response to conversation + conversation.turns.push({ + role: "assistant", + content: aiResponse.response, + results: aiResponse.products, + timestamp: new Date().toISOString(), + }); + + // Trim to last 10 turns for memory efficiency + if (conversation.turns.length > 20) conversation.turns = conversation.turns.slice(-20); + + // Save conversation back to Valkey with TTL + await valkey.call("JSON.SET", convoKey, "$", JSON.stringify(conversation)); + await valkey.expire(convoKey, CONVO_TTL); + track("aiOps"); + + // Record this interaction in Breeth for long-term memory + recordFact(userId, "asked_assistant", message.slice(0, 100), { + sessionId: sid, + intent: aiResponse.intent, + }); + + res.json({ + sessionId: sid, + response: aiResponse.response, + products: aiResponse.products, + followUp: aiResponse.followUp, + intent: aiResponse.intent, + valkeyOp: "JSON.GET + JSON.SET conversation:{sessionId} EX 1800", + storedIn: "Valkey JSON (conversation cache) + Breeth (long-term memory)", + breethMemory: preferences.length > 0 ? "personalized" : "first_visit", + }); + } catch (err) { + res.status(500).json({ error: "ai_error", message: err.message }); + } +}); + +// GET /api/ai/conversation/:sessionId +router.get("/conversation/:sessionId", async (req, res) => { + const { sessionId } = req.params; + const convoKey = `conversation:${sessionId}`; + + try { + const raw = await valkey.call("JSON.GET", convoKey); + track("aiOps"); + + if (!raw) return res.status(404).json({ error: "Conversation not found or expired" }); + + res.json({ ...JSON.parse(raw), valkeyOp: "JSON.GET", storedIn: "Valkey JSON (session cache)" }); + } catch (err) { + res.status(500).json({ error: "conversation_error", message: err.message }); + } +}); + +// POST /api/ai/preferences – record user preference in Breeth +router.post("/preferences", async (req, res) => { + const { userId = "guest:demo", category, action, productId } = req.body; + + try { + await recordFact(userId, action || "viewed", category || productId, { + timestamp: new Date().toISOString(), + }); + + // Also update Valkey affinity score + if (category) { + await valkey.zincrby(`user_affinity:${userId}`, action === "purchase" ? 5 : action === "cart" ? 3 : 1, category); + } + + track("aiOps"); + res.json({ + success: true, + valkeyOp: "ZINCRBY user_affinity:{userId}", + breethOp: "record_fact", + storedIn: "Valkey Sorted Set (affinity) + Breeth (memory graph)", + }); + } catch (err) { + res.status(500).json({ error: "preferences_error", message: err.message }); + } +}); + +// GET /api/ai/recommendations/:userId +router.get("/recommendations/:userId", async (req, res) => { + const { userId } = req.params; + + try { + // Get category affinity from Valkey + const affinityRaw = await valkey.zrevrange(`user_affinity:${userId}`, 0, 4, "WITHSCORES"); + track("aiOps"); + + const affinity = []; + const affinityCats = []; + for (let i = 0; i < affinityRaw.length; i += 2) { + affinity.push({ category: affinityRaw[i], score: Number(affinityRaw[i + 1]) }); + affinityCats.push(affinityRaw[i]); + } + + // Get recently viewed from Valkey list + const recentlyViewed = await valkey.lrange(`user_history:${userId}`, 0, 9); + + // Get Breeth long-term preferences + const breethPrefs = await getUserPreferences(userId); + + // Resolve actual product recommendations + const keys = await valkey.keys("product:*"); + const products = []; + for (const k of keys) { + const rawProd = await valkey.call("JSON.GET", k); + if (rawProd) { + products.push({ id: k, ...JSON.parse(rawProd) }); + } + } + + let recommended = []; + if (affinityCats.length > 0) { + recommended = products.filter(p => affinityCats.includes(p.categoryId)); + } + if (recommended.length < 6) { + const remaining = products + .filter(p => !recommended.some(r => r.id === p.id)) + .sort((a, b) => (b.ratings?.average || 0) - (a.ratings?.average || 0)); + recommended = [...recommended, ...remaining]; + } + + const recommendations = recommended.slice(0, 8).map(p => ({ + id: p.id, + productId: p.id, + name: p.name, + price: p.price?.amount || p.price || 0, + image: p.images?.[0]?.url || "/assets/images/thumbs/product-img1.png", + categoryId: p.categoryId, + brand: p.brand, + ratings: p.ratings, + })); + + res.json({ + userId, + affinity, + recentlyViewed, + breethMemory: breethPrefs.slice(0, 5), + recommendations, + valkeyOp: "ZREVRANGE user_affinity + LRANGE user_history + JSON.GET products", + storedIn: "Valkey Sorted Set (affinity) + List (history) + JSON (products) + Breeth (graph memory)", + }); + } catch (err) { + res.status(500).json({ error: "recommendations_error", message: err.message }); + } +}); + +// POST /api/ai/track-view – track product view for recommendations +router.post("/track-view", async (req, res) => { + const { userId = "guest:demo", productId, categoryId } = req.body; + + try { + // Push to user history list (capped at 50) + if (productId) { + await valkey.lpush(`user_history:${userId}`, productId); + await valkey.ltrim(`user_history:${userId}`, 0, 49); + } + + // Update category affinity + if (categoryId) { + await valkey.zincrby(`user_affinity:${userId}`, 1, categoryId); + } + + // Update trending score + if (productId) { + await valkey.zincrby("trending:global:1h", 1, productId); + await valkey.zincrby("trending:global:24h", 1, productId); + await valkey.expire("trending:global:1h", 3600); + } + + track("aiOps"); + + res.json({ + success: true, + valkeyOp: "LPUSH + LTRIM (history) + ZINCRBY (affinity + trending)", + storedIn: "Valkey List + Sorted Sets", + }); + } catch (err) { + res.status(500).json({ error: "track_error", message: err.message }); + } +}); + +// GET /api/ai/trending +router.get("/trending", async (req, res) => { + try { + const raw = await valkey.zrevrange("trending:global:24h", 0, 9, "WITHSCORES"); + track("aiOps"); + + const trending = []; + for (let i = 0; i < raw.length; i += 2) { + const productId = raw[i]; + const score = Number(raw[i + 1]); + try { + const rawProd = await valkey.call("JSON.GET", productId); + if (rawProd) { + const prod = JSON.parse(rawProd); + trending.push({ + id: productId, + productId, + name: prod.name, + price: prod.price?.amount || prod.price || 0, + image: prod.images?.[0]?.url || "/assets/images/thumbs/product-img1.png", + score, + }); + } + } catch (_) {} + } + + res.json({ + trending, + valkeyOp: "ZREVRANGE trending:global:24h 0 9 WITHSCORES + JSON.GET products", + storedIn: "Valkey Sorted Set (trending) + JSON (products)", + }); + } catch (err) { + res.status(500).json({ error: "trending_error", message: err.message }); + } +}); + +// Smart response generator using product catalog + Breeth preferences +async function generateAIResponse(message, conversation, preferences) { + const msg = message.toLowerCase(); + let intent = "general"; + let response = ""; + let products = []; + let followUp = ""; + + // Fetch available products from Valkey for context + let availableProducts = []; + try { + const keys = await valkey.keys("product:*"); + for (const k of keys.slice(0, 30)) { + const raw = await valkey.call("JSON.GET", k); + if (raw) availableProducts.push({ id: k, ...JSON.parse(raw) }); + } + } catch (_) {} + + // Determine intent from message + if (msg.includes("cheap") || msg.includes("budget") || msg.includes("affordable") || msg.includes("under ")) { + intent = "budget_search"; + const affordable = availableProducts + .sort((a, b) => (a.price?.amount || 0) - (b.price?.amount || 0)) + .slice(0, 4); + products = affordable.map(formatProduct); + response = `Here are some budget-friendly options I found in our catalog:`; + followUp = "Would you like me to filter by a specific category or set a price limit?"; + } else if (msg.includes("phone") || msg.includes("smartphone") || msg.includes("mobile")) { + intent = "category_search"; + const phones = availableProducts.filter(p => + p.name?.toLowerCase().includes("phone") || + p.name?.toLowerCase().includes("galaxy") || + p.categoryId?.includes("smartphone") + ).slice(0, 4); + products = phones.length ? phones.map(formatProduct) : availableProducts.slice(0, 4).map(formatProduct); + response = `Here are our top smartphone picks for you:`; + followUp = "Would you like to see phones under a specific budget, or filter by brand?"; + } else if (msg.includes("laptop") || msg.includes("computer") || msg.includes("pc")) { + intent = "category_search"; + const laptops = availableProducts.filter(p => + p.name?.toLowerCase().includes("laptop") || p.name?.toLowerCase().includes("computer") + ).slice(0, 4); + products = (laptops.length ? laptops : availableProducts.slice(0, 4)).map(formatProduct); + response = `Here are laptop recommendations from our catalog:`; + followUp = "Do you need it for gaming, work, or general use?"; + } else if (msg.includes("recommend") || msg.includes("suggest") || msg.includes("popular") || msg.includes("trending")) { + intent = "recommendations"; + // Use preferences from Breeth if available + const topProducts = availableProducts + .sort((a, b) => (b.ratings?.average || 0) - (a.ratings?.average || 0)) + .slice(0, 4); + products = topProducts.map(formatProduct); + const memNote = preferences.length > 0 ? " Based on your browsing history," : ""; + response = `${memNote} Here are our top-rated products right now:`; + followUp = "Want me to narrow these down by category or price range?"; + } else if (msg.includes("gift") || msg.includes("present") || msg.includes("birthday")) { + intent = "gift_search"; + const giftItems = availableProducts + .sort((a, b) => (b.ratings?.average || 0) - (a.ratings?.average || 0)) + .slice(0, 4); + products = giftItems.map(formatProduct); + response = `Great choice for a gift! Here are some well-rated items that make excellent presents:`; + followUp = "What's your budget? I can help narrow it down to the perfect gift."; + } else if (msg.includes("stock") || msg.includes("available") || msg.includes("in stock")) { + intent = "availability_check"; + const inStock = availableProducts.filter(p => (p.inventory?.quantity || 0) > (p.inventory?.reserved || 0)).slice(0, 4); + products = inStock.map(formatProduct); + response = `Here are products currently in stock with real-time inventory powered by Valkey:`; + followUp = "Would you like to check availability for a specific product?"; + } else { + intent = "general"; + // Search by message keywords + const keywords = msg.split(" ").filter(w => w.length > 3); + const matched = availableProducts.filter(p => + keywords.some(kw => p.name?.toLowerCase().includes(kw) || p.description?.toLowerCase().includes(kw)) + ); + products = (matched.length ? matched : availableProducts).slice(0, 4).map(formatProduct); + response = `I found some products that might match what you're looking for:`; + followUp = "Can you tell me more about what you need? I can search by price, brand, or category."; + } + + // Personalize with Breeth preferences + if (preferences.length > 0) { + response += " (personalized based on your Breeth memory profile)"; + } + + return { response, products, intent, followUp }; +} + +function formatProduct(p) { + return { + productId: p.id, + name: p.name, + price: p.price?.amount || 0, + currency: p.price?.currency || "INR", + rating: p.ratings?.average || 0, + image: p.images?.[0]?.url || "/assets/images/thumbs/product-img1.png", + inStock: (p.inventory?.quantity || 0) > (p.inventory?.reserved || 0), + brand: p.brand, + }; +} + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/analytics.js b/submissions/valkey-ecommerce-demo/backend/src/routes/analytics.js new file mode 100644 index 0000000..34f5e07 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/analytics.js @@ -0,0 +1,330 @@ +/** + * Analytics – aggregates data from Valkey for dashboard metrics + * + * Valkey commands: + * ZREVRANGE trending:global:24h 0 9 WITHSCORES – trending products + * FT.AGGREGATE idx:products – category distribution + * KEYS product:* + JSON.GET $.inventory – inventory alerts + * KEYS user:*, session:*, cart:*, wishlist:* – user stats + * HGETALL cart:* – revenue estimate + cart abandonment + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, getStats } = require("../valkey"); + +// GET /api/analytics +router.get("/", async (req, res) => { + try { + const valkeyOps = []; + + // 1. Trending products + let trendingProducts = []; + try { + const trendingRaw = await valkey.zrevrange("trending:global:24h", 0, 9, "WITHSCORES"); + valkeyOps.push("ZREVRANGE trending:global:24h 0 9 WITHSCORES"); + + for (let i = 0; i < trendingRaw.length; i += 2) { + const pid = trendingRaw[i]; + const score = parseFloat(trendingRaw[i + 1]); + let name = pid; + try { + const raw = await valkey.call("JSON.GET", pid, "$.name"); + if (raw) { + const parsed = JSON.parse(raw); + name = Array.isArray(parsed) ? parsed[0] : parsed; + } + } catch (_) {} + trendingProducts.push({ productId: pid, name, score }); + } + } catch (e) { + valkeyOps.push("ZREVRANGE trending:global:24h (error: " + e.message + ")"); + } + + // 2. Top categories via FT.AGGREGATE + let topCategories = []; + try { + const catAgg = await valkey.call( + "FT.AGGREGATE", "idx:products", "*", + "GROUPBY", "1", "@categoryId", + "REDUCE", "COUNT", "0", "AS", "count", + "SORTBY", "2", "@count", "DESC", + "LIMIT", "0", "5" + ); + valkeyOps.push("FT.AGGREGATE idx:products GROUPBY @categoryId REDUCE COUNT"); + + for (let i = 1; i < catAgg.length; i++) { + const row = catAgg[i]; + const obj = {}; + for (let j = 0; j < row.length; j += 2) { + obj[row[j]] = row[j + 1]; + } + if (obj.categoryId) { + // Fetch category name + let catName = obj.categoryId; + try { + const catRaw = await valkey.call("JSON.GET", obj.categoryId, "$.name"); + if (catRaw) { + const parsed = JSON.parse(catRaw); + catName = Array.isArray(parsed) ? parsed[0] : parsed; + } + } catch (_) {} + topCategories.push({ categoryId: obj.categoryId, name: catName, productCount: Number(obj.count) }); + } + } + } catch (e) { + valkeyOps.push("FT.AGGREGATE (error: " + e.message + ")"); + topCategories = []; + } + + // 3. Inventory alerts: products where (quantity - reserved) < 10 + let inventoryAlerts = []; + try { + const productKeys = await valkey.keys("product:*"); + valkeyOps.push(`KEYS product:* → found ${productKeys.length} products`); + + for (const pk of productKeys) { + try { + const invRaw = await valkey.call("JSON.GET", pk, "$.inventory"); + if (invRaw) { + const invArr = JSON.parse(invRaw); + const inv = Array.isArray(invArr) ? invArr[0] : invArr; + if (inv && typeof inv.quantity === "number") { + const available = inv.quantity - (inv.reserved || 0); + if (available < 10) { + let pName = pk; + try { + const nameRaw = await valkey.call("JSON.GET", pk, "$.name"); + if (nameRaw) { + const parsed = JSON.parse(nameRaw); + pName = Array.isArray(parsed) ? parsed[0] : parsed; + } + } catch (_) {} + inventoryAlerts.push({ + productId: pk, + name: pName, + quantity: inv.quantity, + reserved: inv.reserved || 0, + available, + warehouse: inv.warehouse || "unknown", + alertLevel: available <= 0 ? "out_of_stock" : available < 5 ? "critical" : "low", + }); + } + } + } + } catch (_) {} + } + valkeyOps.push("JSON.GET $.inventory for each product"); + } catch (e) { + valkeyOps.push("Inventory scan (error: " + e.message + ")"); + } + + // 4. User stats: count keys by type + let userStats = {}; + try { + const [userKeys, sessionKeys, cartKeys, wishlistKeys] = await Promise.all([ + valkey.keys("user:*"), + valkey.keys("session:*"), + valkey.keys("cart:*"), + valkey.keys("wishlist:*"), + ]); + valkeyOps.push("KEYS user:*, KEYS session:*, KEYS cart:*, KEYS wishlist:*"); + userStats = { + totalUsers: userKeys.length, + activeSessions: sessionKeys.length, + activeCarts: cartKeys.length, + activeWishlists: wishlistKeys.length, + }; + } catch (e) { + valkeyOps.push("User stats KEYS (error: " + e.message + ")"); + userStats = { totalUsers: 0, activeSessions: 0, activeCarts: 0, activeWishlists: 0 }; + } + + // 5. Revenue estimate + cart abandonment + let revenueEstimate = 0; + let cartAbandonment = 0; + let cartsWithItems = 0; + try { + const cartKeys = await valkey.keys("cart:*"); + valkeyOps.push("HGETALL each cart:* for revenue estimate"); + + for (const cartKey of cartKeys) { + try { + const cartData = await valkey.hgetall(cartKey); + if (cartData && Object.keys(cartData).length > 0) { + cartsWithItems++; + for (const val of Object.values(cartData)) { + try { + const item = JSON.parse(val); + if (item.price && item.quantity) { + revenueEstimate += item.price * item.quantity; + } + } catch (_) {} + } + } + } catch (_) {} + } + cartAbandonment = cartsWithItems; // carts with items = potential abandonment + } catch (e) { + valkeyOps.push("Cart revenue scan (error: " + e.message + ")"); + } + + // 6. Search terms (mock/tracked data) + let searchTerms = []; + try { + const searchKeys = await valkey.keys("search:term:*"); + if (searchKeys.length > 0) { + for (const sk of searchKeys.slice(0, 10)) { + const count = await valkey.get(sk); + const term = sk.replace("search:term:", ""); + searchTerms.push({ term, count: Number(count) }); + } + searchTerms.sort((a, b) => b.count - a.count); + valkeyOps.push("KEYS search:term:* + GET each"); + } else { + // Demo data when no tracking exists + searchTerms = [ + { term: "iphone", count: 142, source: "demo" }, + { term: "laptop", count: 98, source: "demo" }, + { term: "headphones", count: 87, source: "demo" }, + { term: "samsung", count: 76, source: "demo" }, + { term: "macbook", count: 65, source: "demo" }, + ]; + valkeyOps.push("KEYS search:term:* (no data — returning demo search terms)"); + } + } catch (e) { + valkeyOps.push("Search terms (error: " + e.message + ")"); + } + + // 7. Feature usage from valkey.js getStats() + const featureUsage = getStats(); + valkeyOps.push("getStats() from valkey.js usage counters"); + + res.json({ + timestamp: new Date().toISOString(), + trendingProducts, + topCategories, + inventoryAlerts: { + count: inventoryAlerts.length, + items: inventoryAlerts, + }, + userStats, + revenueEstimate: { + amount: revenueEstimate, + currency: "INR", + cartsWithItems, + note: "Sum of all active cart item prices * quantities", + }, + cartAbandonment: { + cartsWithItems, + note: "Carts with items that have not been checked out", + }, + searchTerms, + featureUsage, + valkeyOps, + storedIn: "Valkey Sorted Set, Search Index, JSON, Hash, String, Set", + }); + } catch (err) { + res.status(500).json({ error: "analytics_error", message: err.message }); + } +}); + +// GET /api/analytics/conversion — funnel data +router.get("/conversion", async (req, res) => { + try { + const valkeyOps = []; + + // Views: total trending score = proxy for total views + let totalViews = 0; + let uniqueProductViews = 0; + try { + const trendingMembers = await valkey.zrevrange("trending:global:24h", 0, -1, "WITHSCORES"); + valkeyOps.push("ZREVRANGE trending:global:24h 0 -1 WITHSCORES"); + uniqueProductViews = trendingMembers.length / 2; + for (let i = 1; i < trendingMembers.length; i += 2) { + totalViews += parseFloat(trendingMembers[i]); + } + } catch (e) { + valkeyOps.push("ZREVRANGE (error: " + e.message + ")"); + } + + // Cart adds: count carts with items + let cartsWithItems = 0; + let totalCartItems = 0; + try { + const cartKeys = await valkey.keys("cart:*"); + valkeyOps.push(`KEYS cart:* → ${cartKeys.length} carts`); + for (const ck of cartKeys) { + try { + const cartData = await valkey.hgetall(ck); + if (cartData && Object.keys(cartData).length > 0) { + cartsWithItems++; + totalCartItems += Object.keys(cartData).length; + } + } catch (_) {} + } + valkeyOps.push("HGETALL each cart:* for item counts"); + } catch (e) { + valkeyOps.push("Cart keys (error: " + e.message + ")"); + } + + // Wishlist adds + let wishlistCount = 0; + try { + const wishlistKeys = await valkey.keys("wishlist:*"); + valkeyOps.push(`KEYS wishlist:* → ${wishlistKeys.length} wishlists`); + for (const wk of wishlistKeys) { + try { + const wData = await valkey.hgetall(wk); + if (wData) wishlistCount += Object.keys(wData).length; + } catch (_) {} + } + } catch (e) { + valkeyOps.push("Wishlist keys (error: " + e.message + ")"); + } + + // Estimate checkout conversions (10% of carts as demo data) + const checkoutEstimate = Math.ceil(cartsWithItems * 0.1); + + // Conversion rates + const cartConversionRate = totalViews > 0 ? ((cartsWithItems / totalViews) * 100).toFixed(2) : "0.00"; + const checkoutConversionRate = cartsWithItems > 0 ? ((checkoutEstimate / cartsWithItems) * 100).toFixed(2) : "0.00"; + + res.json({ + timestamp: new Date().toISOString(), + funnel: { + productViews: { + total: Math.round(totalViews), + uniqueProducts: uniqueProductViews, + valkeySource: "ZREVRANGE trending:global:24h WITHSCORES (cumulative score = views proxy)", + }, + addToCart: { + cartSessions: cartsWithItems, + totalItemsInCarts: totalCartItems, + conversionRate: `${cartConversionRate}%`, + valkeySource: "KEYS cart:* + HGETALL", + }, + wishlist: { + totalWishlistItems: wishlistCount, + valkeySource: "KEYS wishlist:* + HGETALL", + }, + checkout: { + estimatedCheckouts: checkoutEstimate, + conversionRate: `${checkoutConversionRate}%`, + note: "Estimated at 10% of active carts (demo data)", + }, + }, + summary: { + viewToCartRate: `${cartConversionRate}%`, + cartToCheckoutRate: `${checkoutConversionRate}%`, + overallConversionRate: totalViews > 0 ? ((checkoutEstimate / totalViews) * 100).toFixed(3) + "%" : "0.000%", + }, + valkeyOps, + storedIn: "Valkey Sorted Set (trending), Hash (carts, wishlists)", + }); + } catch (err) { + res.status(500).json({ error: "conversion_analytics_error", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/auth.js b/submissions/valkey-ecommerce-demo/backend/src/routes/auth.js new file mode 100644 index 0000000..8580901 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/auth.js @@ -0,0 +1,186 @@ +/** + * User Authentication – Valkey String (session tokens) + * + * Valkey commands used: + * JSON.SET {userId} – store user profile as JSON document + * SET session:{token} {userId} EX 86400 – session with 24h TTL + * GET session:{token} – validate session + * EXPIRE session:{token} ... – refresh TTL on activity + * DEL session:{token} – logout + * INCR login_attempts:{email} – rate limiting + * EXPIRE login_attempts:{email} 900 – 15 min window + */ + +const express = require("express"); +const router = express.Router(); +const bcrypt = require("bcryptjs"); +const { v7: uuidv7 } = require("uuid"); +const { valkey, track } = require("../valkey"); +const { recordFact } = require("../breeth"); + +const SESSION_TTL = 86400; // 24 hours +const RATE_LIMIT_MAX = 5; +const RATE_LIMIT_WINDOW = 900; // 15 min + +// POST /api/auth/register +router.post("/register", async (req, res) => { + const { email, password, firstName, lastName } = req.body; + if (!email || !password) return res.status(400).json({ error: "email and password required" }); + + try { + // Check if email already registered + const existingId = await valkey.get(`email:${email}`); + if (existingId) return res.status(409).json({ error: "Email already registered" }); + + const userId = `user:${uuidv7()}`; + const passwordHash = await bcrypt.hash(password, 12); + + const user = { + id: userId, + email, + passwordHash, + firstName: firstName || "", + lastName: lastName || "", + role: "customer", + createdAt: new Date().toISOString(), + }; + + // Store user JSON in Valkey + await valkey.call("JSON.SET", userId, "$", JSON.stringify(user)); + // Email → userId index + await valkey.set(`email:${email}`, userId); + track("sessionOps"); + + // Record user creation in Breeth for memory + recordFact(userId, "registered", "ecommerce_platform", { email, firstName }); + + res.status(201).json({ + success: true, + userId, + valkeyOp: "JSON.SET + SET (email index)", + storedIn: "Valkey JSON", + }); + } catch (err) { + res.status(500).json({ error: "register_error", message: err.message }); + } +}); + +// POST /api/auth/login +router.post("/login", async (req, res) => { + const { email, password } = req.body; + if (!email || !password) return res.status(400).json({ error: "email and password required" }); + + try { + // Rate limiting check + const attemptsKey = `login_attempts:${email}`; + const attempts = await valkey.incr(attemptsKey); + await valkey.expire(attemptsKey, RATE_LIMIT_WINDOW); + track("sessionOps"); + + if (attempts > RATE_LIMIT_MAX) { + return res.status(429).json({ + error: "rate_limit_exceeded", + message: "Too many login attempts. Try again in 15 minutes.", + valkeyOp: "INCR (rate limiting)", + storedIn: "Valkey String counter", + }); + } + + const userId = await valkey.get(`email:${email}`); + if (!userId) return res.status(401).json({ error: "Invalid credentials" }); + + const raw = await valkey.call("JSON.GET", userId); + if (!raw) return res.status(401).json({ error: "User not found" }); + + const user = JSON.parse(raw); + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) return res.status(401).json({ error: "Invalid credentials" }); + + // Reset rate limit on success + await valkey.del(attemptsKey); + + // Create session token + const token = uuidv7(); + await valkey.set(`session:${token}`, userId, "EX", SESSION_TTL); + track("sessionOps"); + + // Update lastLoginAt + await valkey.call("JSON.SET", userId, "$.lastLoginAt", JSON.stringify(new Date().toISOString())); + + recordFact(userId, "logged_in", "ecommerce_platform", { timestamp: new Date().toISOString() }); + + res.json({ + success: true, + token, + userId, + user: { id: userId, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role }, + valkeyOp: "SET session:{token} {userId} EX 86400", + storedIn: "Valkey String (session)", + sessionTTL: `${SESSION_TTL}s (24 hours)`, + }); + } catch (err) { + res.status(500).json({ error: "login_error", message: err.message }); + } +}); + +// GET /api/auth/me +router.get("/me", async (req, res) => { + const token = req.headers.authorization?.replace("Bearer ", ""); + if (!token) return res.status(401).json({ error: "No token provided" }); + + try { + const userId = await valkey.get(`session:${token}`); + track("sessionOps"); + + if (!userId) return res.status(401).json({ error: "Session expired or invalid" }); + + // Refresh TTL + await valkey.expire(`session:${token}`, SESSION_TTL); + + const raw = await valkey.call("JSON.GET", userId); + if (!raw) return res.status(404).json({ error: "User not found" }); + + const user = JSON.parse(raw); + const { passwordHash, ...safeUser } = user; + + res.json({ + user: safeUser, + valkeyOp: "GET session:{token} → JSON.GET {userId}", + storedIn: "Valkey String session + JSON user profile", + }); + } catch (err) { + res.status(500).json({ error: "auth_error", message: err.message }); + } +}); + +// POST /api/auth/logout +router.post("/logout", async (req, res) => { + const token = req.headers.authorization?.replace("Bearer ", ""); + if (!token) return res.status(400).json({ error: "No token" }); + + try { + await valkey.del(`session:${token}`); + track("sessionOps"); + res.json({ success: true, valkeyOp: "DEL session:{token}", storedIn: "Valkey String session" }); + } catch (err) { + res.status(500).json({ error: "logout_error", message: err.message }); + } +}); + +// POST /api/auth/refresh +router.post("/refresh", async (req, res) => { + const token = req.headers.authorization?.replace("Bearer ", ""); + if (!token) return res.status(400).json({ error: "No token" }); + + try { + const exists = await valkey.expire(`session:${token}`, SESSION_TTL); + track("sessionOps"); + + if (!exists) return res.status(401).json({ error: "Session not found" }); + res.json({ success: true, ttl: SESSION_TTL, valkeyOp: "EXPIRE session:{token}", storedIn: "Valkey String session" }); + } catch (err) { + res.status(500).json({ error: "refresh_error", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/cart.js b/submissions/valkey-ecommerce-demo/backend/src/routes/cart.js new file mode 100644 index 0000000..3f9859c --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/cart.js @@ -0,0 +1,188 @@ +/** + * Cart Management – Valkey Hash + * + * Key: cart:{userId} + * Field: productId → JSON string with { quantity, name, price, image } + * TTL: 7 days (604800s) — abandoned carts auto-expire + * + * Valkey commands used: + * HSET – add / update item + * HGETALL – fetch full cart + * HDEL – remove item + * DEL – clear cart + * EXPIRE – reset abandon TTL + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, track } = require("../valkey"); +const { recordFact } = require("../breeth"); + +const CART_TTL = 604800; // 7 days + +function cartKey(userId) { + return `cart:${userId}`; +} + +// GET /api/cart?userId=xxx +router.get("/", async (req, res) => { + const userId = req.query.userId || req.headers["x-user-id"] || "guest:demo"; + try { + const raw = await valkey.hgetall(cartKey(userId)); + track("cartOps"); + + const items = raw + ? Object.entries(raw).map(([productId, val]) => ({ + productId, + ...JSON.parse(val), + })) + : []; + + const subtotal = items.reduce((s, i) => s + i.price * i.quantity, 0); + + res.json({ + userId, + items, + subtotal, + itemCount: items.length, + valkeyKey: cartKey(userId), + valkeyOp: "HGETALL", + storedIn: "Valkey Hash", + }); + } catch (err) { + res.status(500).json({ error: "cart_error", message: err.message }); + } +}); + +// POST /api/cart/items +router.post("/items", async (req, res) => { + const { userId = "guest:demo", productId, name, price, image, quantity = 1 } = req.body; + if (!productId) return res.status(400).json({ error: "productId required" }); + + try { + const key = cartKey(userId); + + // Check if item already exists; merge quantity + const existing = await valkey.hget(key, productId); + const prev = existing ? JSON.parse(existing) : null; + const newQty = prev ? prev.quantity + quantity : quantity; + + const item = { name, price, image, quantity: newQty }; + await valkey.hset(key, productId, JSON.stringify(item)); + await valkey.expire(key, CART_TTL); + track("cartOps"); + + // Track in Breeth for personalization + recordFact(userId, "added_to_cart", productId, { quantity: newQty, price }); + + res.json({ + success: true, + productId, + quantity: newQty, + valkeyKey: key, + valkeyOp: "HSET + EXPIRE", + storedIn: "Valkey Hash (cart)", + }); + } catch (err) { + res.status(500).json({ error: "cart_error", message: err.message }); + } +}); + +// PATCH /api/cart/items/:productId +router.patch("/items/:productId", async (req, res) => { + const { userId = "guest:demo", quantity } = req.body; + const { productId } = req.params; + + try { + const key = cartKey(userId); + const existing = await valkey.hget(key, productId); + if (!existing) return res.status(404).json({ error: "Item not in cart" }); + + const item = JSON.parse(existing); + item.quantity = quantity; + await valkey.hset(key, productId, JSON.stringify(item)); + await valkey.expire(key, CART_TTL); + track("cartOps"); + + res.json({ success: true, productId, quantity, valkeyOp: "HSET", storedIn: "Valkey Hash" }); + } catch (err) { + res.status(500).json({ error: "cart_error", message: err.message }); + } +}); + +// DELETE /api/cart/items/:productId +router.delete("/items/:productId", async (req, res) => { + const userId = req.query.userId || req.headers["x-user-id"] || "guest:demo"; + const { productId } = req.params; + + try { + await valkey.hdel(cartKey(userId), productId); + track("cartOps"); + res.json({ success: true, valkeyOp: "HDEL", storedIn: "Valkey Hash" }); + } catch (err) { + res.status(500).json({ error: "cart_error", message: err.message }); + } +}); + +// DELETE /api/cart – clear cart +router.delete("/", async (req, res) => { + const userId = req.query.userId || req.headers["x-user-id"] || "guest:demo"; + try { + await valkey.del(cartKey(userId)); + track("cartOps"); + res.json({ success: true, valkeyOp: "DEL", storedIn: "Valkey Hash" }); + } catch (err) { + res.status(500).json({ error: "cart_error", message: err.message }); + } +}); + +// POST /api/cart/coupon – validate & apply coupon +router.post("/coupon", async (req, res) => { + const { userId = "guest:demo", couponCode, subtotal } = req.body; + if (!couponCode) return res.status(400).json({ error: "couponCode required" }); + + try { + const raw = await valkey.call("JSON.GET", `coupon:${couponCode.toUpperCase()}`); + track("cartOps"); + + if (!raw) return res.status(404).json({ error: "Coupon not found" }); + + const coupon = JSON.parse(raw); + const now = new Date(); + + if (!coupon.active) return res.status(400).json({ error: "Coupon inactive" }); + if (new Date(coupon.validUntil) < now) return res.status(400).json({ error: "Coupon expired" }); + if (coupon.usedCount >= coupon.usageLimit) return res.status(400).json({ error: "Coupon usage limit reached" }); + if (subtotal < coupon.minOrderAmount) { + return res.status(400).json({ + error: "Minimum order not met", + required: coupon.minOrderAmount, + current: subtotal, + }); + } + + let discount = 0; + if (coupon.type === "percentage") { + discount = Math.min((subtotal * coupon.value) / 100, coupon.maxDiscount); + } else { + discount = coupon.value; + } + + // Check if user already used this coupon + const alreadyUsed = await valkey.sismember(`coupon_used:${couponCode.toUpperCase()}`, userId); + if (alreadyUsed) return res.status(400).json({ error: "Coupon already used by this user" }); + + res.json({ + success: true, + coupon: { code: coupon.code, type: coupon.type, value: coupon.value }, + discount: Math.round(discount), + finalTotal: subtotal - Math.round(discount), + valkeyOp: "JSON.GET + SISMEMBER", + storedIn: "Valkey JSON + Set", + }); + } catch (err) { + res.status(500).json({ error: "coupon_error", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/comparison.js b/submissions/valkey-ecommerce-demo/backend/src/routes/comparison.js new file mode 100644 index 0000000..894f19c --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/comparison.js @@ -0,0 +1,375 @@ +/** + * Product Comparison – Valkey SET per user + * key: compare:{userId} (max 4 items) + * + * Valkey commands: + * SMEMBERS compare:{userId} – get all compared product IDs + * SCARD compare:{userId} – check count before adding + * SADD compare:{userId} {id} – add product to compare set + * SREM compare:{userId} {id} – remove product from compare set + * DEL compare:{userId} – clear compare list + * JSON.GET {productId} – fetch product details + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, track } = require("../valkey"); + +const MAX_COMPARE = 4; + +// GET /api/comparison?userId= +router.get("/", async (req, res) => { + const { userId } = req.query; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `compare:${userId}`; + try { + const productIds = await valkey.smembers(key); + + const products = []; + for (const pid of productIds) { + try { + const raw = await valkey.call("JSON.GET", pid); + if (raw) { + const product = JSON.parse(raw); + products.push(product); + } + } catch (e) { + products.push({ id: pid, error: "product_not_found" }); + } + } + + res.json({ + userId, + count: products.length, + maxAllowed: MAX_COMPARE, + products, + valkeyOp: `SMEMBERS ${key} → JSON.GET each product`, + storedIn: "Valkey Set (compare list) + Valkey JSON (products)", + }); + } catch (err) { + res.status(500).json({ error: "comparison_fetch_error", message: err.message }); + } +}); + +// POST /api/comparison/add +router.post("/add", async (req, res) => { + const { userId, productId } = req.body; + if (!userId || !productId) { + return res.status(400).json({ error: "userId and productId required" }); + } + + const key = `compare:${userId}`; + try { + // Check current count + const currentCount = await valkey.scard(key); + if (currentCount >= MAX_COMPARE) { + return res.status(400).json({ + error: "comparison_limit_reached", + message: `Maximum ${MAX_COMPARE} products can be compared at once`, + currentCount, + maxAllowed: MAX_COMPARE, + valkeyOp: `SCARD ${key}`, + storedIn: "Valkey Set", + }); + } + + const added = await valkey.sadd(key, productId); + await valkey.expire(key, 86400); // 24h TTL + + // Fetch product details to confirm + let product = null; + try { + const raw = await valkey.call("JSON.GET", productId); + if (raw) product = JSON.parse(raw); + } catch (_) {} + + res.json({ + success: true, + added: added === 1, + alreadyPresent: added === 0, + userId, + productId, + product: product ? { id: product.id, name: product.name, price: product.price, brand: product.brand } : null, + newCount: await valkey.scard(key), + maxAllowed: MAX_COMPARE, + valkeyOp: `SCARD ${key} → SADD ${key} ${productId}`, + storedIn: "Valkey Set (compare:{userId})", + }); + } catch (err) { + res.status(500).json({ error: "comparison_add_error", message: err.message }); + } +}); + +// DELETE /api/comparison/remove +router.delete("/remove", async (req, res) => { + const { userId, productId } = req.body; + if (!userId || !productId) { + return res.status(400).json({ error: "userId and productId required" }); + } + + const key = `compare:${userId}`; + try { + const removed = await valkey.srem(key, productId); + + res.json({ + success: true, + removed: removed === 1, + userId, + productId, + remainingCount: await valkey.scard(key), + valkeyOp: `SREM ${key} ${productId}`, + storedIn: "Valkey Set (compare:{userId})", + }); + } catch (err) { + res.status(500).json({ error: "comparison_remove_error", message: err.message }); + } +}); + +// DELETE /api/comparison — clear all +router.delete("/", async (req, res) => { + const { userId } = req.body; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `compare:${userId}`; + try { + await valkey.del(key); + + res.json({ + success: true, + userId, + valkeyOp: `DEL ${key}`, + storedIn: "Valkey Set (compare:{userId})", + }); + } catch (err) { + res.status(500).json({ error: "comparison_clear_error", message: err.message }); + } +}); + +// POST /api/comparison/ai-compare +router.post("/ai-compare", async (req, res) => { + const { userId } = req.body; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `compare:${userId}`; + try { + const productIds = await valkey.smembers(key); + if (productIds.length < 2) { + return res.status(400).json({ error: "At least 2 products are required to compare." }); + } + + // Fetch all products from Valkey JSON + const products = []; + for (const pid of productIds) { + try { + const raw = await valkey.call("JSON.GET", pid); + if (raw) products.push(JSON.parse(raw)); + } catch (_) {} + } + + if (products.length < 2) { + return res.status(400).json({ error: "Could not load product details from Valkey." }); + } + + // ── Try Breeth AI ───────────────────────────────────────────────────────── + let comparison = null; + let poweredBy = "Smart Analysis Engine (Valkey data)"; + let aiNote = null; + + try { + const https = require("https"); + + const productDescriptions = products + .map(p => [ + `Name: ${p.name}`, + `Brand: ${p.brand || "Unknown"}`, + `Price: ₹${(p.price?.amount || p.price || 0).toLocaleString("en-IN")}`, + `Rating: ${p.ratings?.average || 4.5} stars (${p.ratings?.count || 0} reviews)`, + `Category: ${p.categoryId || "General"}`, + `Description: ${p.shortDescription || p.description || "N/A"}`, + ].join(", ")) + .join("\n\n"); + + const prompt = + `You are an expert shopping assistant. Compare the following products side-by-side. ` + + `Highlight key differences in price, features, value, and recommend which to buy under different circumstances. ` + + `Give a clear, structured response in 3-4 short paragraphs.\n\nProducts:\n${productDescriptions}`; + + const payload = JSON.stringify({ + query: prompt, + context: { action: "product_comparison", productCount: products.length, userId }, + }); + + const breethResponse = await new Promise((resolve, reject) => { + const options = { + hostname: "api.thebreeth.com", + path: "/v1/search", + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${process.env.BREETH_API_KEY || "ck_live_YhQWb-ZjIQxfs6AzDezC3zaPMPw4xJ8yRDkjIz7d16A"}`, + "Content-Length": Buffer.byteLength(payload), + }, + }; + + const request = https.request(options, (resp) => { + // Reject immediately on HTTP 4xx/5xx + if (resp.statusCode >= 400) { + request.destroy(); + return reject(new Error(`Breeth API HTTP ${resp.statusCode}`)); + } + let data = ""; + resp.on("data", (chunk) => { data += chunk; }); + resp.on("end", () => { + // Reject if the body is an HTML page (e.g. gateway error) + if (data.trim().startsWith("<")) { + return reject(new Error("Breeth returned HTML (gateway error)")); + } + try { + const parsed = JSON.parse(data); + const text = parsed.response || parsed.text || parsed.answer || parsed.message; + if (!text) return reject(new Error("Breeth returned empty body")); + resolve(text); + } catch (_) { + reject(new Error("Breeth returned non-JSON body")); + } + }); + }); + + request.on("error", reject); + request.setTimeout(8000, () => { + request.destroy(); + reject(new Error("Breeth AI timeout after 8s")); + }); + request.write(payload); + request.end(); + }); + + comparison = breethResponse; + poweredBy = "Breeth AI"; + + } catch (e) { + // ── Rich Smart Fallback — fully derived from Valkey product data ────── + aiNote = `Breeth AI unavailable (${e.message}). Showing data-driven comparison from Valkey.`; + comparison = buildSmartComparison(products); + } + + track("aiOps"); + + res.json({ + success: true, + comparison, + poweredBy, + valkeyOp: `SMEMBERS ${key} + JSON.GET per product`, + storedIn: "Valkey Set (compare:{userId}) + Valkey JSON (products)", + ...(aiNote ? { aiNote } : {}), + }); + + } catch (err) { + res.status(500).json({ error: "ai_comparison_error", message: err.message }); + } +}); + +/** + * Build a rich, structured product comparison from Valkey JSON product data. + * Returns a markdown-formatted string. + */ +function buildSmartComparison(products) { + if (!products || products.length < 2) return "Not enough products to compare."; + + const fmt = (n) => Number(n || 0).toLocaleString("en-IN"); + const prices = products.map(p => Number(p.price?.amount || p.price || 0)); + const ratings = products.map(p => Number(p.ratings?.average || 4.0)); + const reviews = products.map(p => Number(p.ratings?.count || 0)); + const stocks = products.map(p => { + const inv = p.inventory; + return inv ? (Number(inv.quantity) || 0) - (Number(inv.reserved) || 0) : null; + }); + + const cheapestIdx = prices.indexOf(Math.min(...prices)); + const highestRatedIdx = ratings.indexOf(Math.max(...ratings)); + const mostReviewedIdx = reviews.indexOf(Math.max(...reviews)); + + const lines = []; + + // ── Price Comparison ────────────────────────────────────────────────────── + lines.push("## 💰 Price Comparison\n"); + products.forEach((p, i) => { + const diff = prices[i] - prices[cheapestIdx]; + const tag = i === cheapestIdx + ? " 🏷️ **Best Price**" + : ` (+₹${fmt(diff)} over cheapest)`; + lines.push(`- **${p.name}** (${p.brand || "Unknown"}) — ₹${fmt(prices[i])}${tag}`); + }); + const delta = Math.max(...prices) - Math.min(...prices); + lines.push(`\n${products[cheapestIdx].name} is the most affordable, ₹${fmt(delta)} cheaper than the priciest option here.\n`); + + // ── Ratings & Reviews ───────────────────────────────────────────────────── + lines.push("## ⭐ Ratings & Customer Trust\n"); + products.forEach((p, i) => { + const filled = Math.round(ratings[i]); + const stars = "★".repeat(filled) + "☆".repeat(Math.max(0, 5 - filled)); + lines.push(`- **${p.name}** — ${stars} ${ratings[i].toFixed(1)}/5 · ${fmt(reviews[i])} reviews`); + }); + const ratingNote = mostReviewedIdx !== highestRatedIdx + ? ` ${products[mostReviewedIdx].name} has the most reviews (${fmt(reviews[mostReviewedIdx])}), indicating broader adoption.` + : ""; + lines.push(`\n${products[highestRatedIdx].name} leads in customer satisfaction with ${ratings[highestRatedIdx].toFixed(1)}★.${ratingNote}\n`); + + // ── Stock Availability ──────────────────────────────────────────────────── + if (stocks.some(s => s !== null)) { + lines.push("## 📦 Stock Availability\n"); + products.forEach((p, i) => { + const s = stocks[i]; + const level = s === null ? "ℹ️ Unknown" + : s <= 0 ? "❌ Out of Stock" + : s < 5 ? `⚠️ Critical — only ${s} left` + : s < 20 ? `🟡 Low Stock (${s} units)` + : `✅ In Stock (${s} units)`; + lines.push(`- **${p.name}** — ${level}`); + }); + lines.push(""); + } + + // ── Feature Snapshot ────────────────────────────────────────────────────── + lines.push("## 🔍 Feature Snapshot\n"); + products.forEach((p) => { + const desc = p.shortDescription || p.description || ""; + const snippet = desc.length > 150 ? desc.slice(0, 150) + "…" : desc; + lines.push(`**${p.name}:** ${snippet || "No description available."}\n`); + }); + + // ── Overall Recommendation ──────────────────────────────────────────────── + lines.push("## 🏆 Our Recommendation\n"); + + // Score: best price = 3 pts, highest rating = 2 pts, most reviews = 1 pt + const scores = products.map(() => 0); + scores[cheapestIdx] += 3; + scores[highestRatedIdx] += 2; + scores[mostReviewedIdx] += 1; + const winnerIdx = scores.indexOf(Math.max(...scores)); + const winner = products[winnerIdx]; + + lines.push(`🥇 **Top Pick: ${winner.name}** (${winner.brand || "Unknown Brand"})`); + + if (winnerIdx === cheapestIdx && winnerIdx === highestRatedIdx) { + lines.push(`\nIt offers both the lowest price (₹${fmt(prices[winnerIdx])}) and the highest customer rating (${ratings[winnerIdx].toFixed(1)}★) — a clear best-value choice for most shoppers.`); + } else if (winnerIdx === cheapestIdx) { + lines.push(`\nWhile ${products[highestRatedIdx].name} edges ahead on ratings, ${winner.name} wins on affordability at ₹${fmt(prices[winnerIdx])}, making it ideal for budget-conscious buyers.`); + } else { + lines.push(`\nAt ₹${fmt(prices[winnerIdx])} with a ${ratings[winnerIdx].toFixed(1)}★ rating, it strikes the best overall balance of quality and value in this comparison.`); + } + + // Alternative mention + const alts = products.filter((_, i) => i !== winnerIdx); + if (alts.length > 0) { + const alt = alts[0]; + const altIdx = products.indexOf(alt); + const reason = altIdx === highestRatedIdx ? "top ratings" : "competitive pricing"; + lines.push(`\n💡 **Alternative:** For ${reason}, consider **${alt.name}** — ${ratings[altIdx].toFixed(1)}★ at ₹${fmt(prices[altIdx])}.`); + } + + return lines.join("\n"); +} + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/inventory.js b/submissions/valkey-ecommerce-demo/backend/src/routes/inventory.js new file mode 100644 index 0000000..6e565b7 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/inventory.js @@ -0,0 +1,171 @@ +/** + * Real-time Inventory Management – Valkey JSON + Lua atomic scripts + * + * Valkey commands used: + * JSON.GET {productId} $.inventory – read stock + * JSON.NUMINCRBY {productId} $.inventory.reserved +N – reserve stock + * JSON.NUMINCRBY {productId} $.inventory.quantity -N – commit decrement + * EVAL Lua – atomic check-and-reserve to prevent overselling + * SET reservation:{orderId}:{productId} {qty} EX 600 – 10-min reservation TTL + * ZINCRBY trending:global:1h – update trending on stock changes + */ + +const express = require("express"); +const router = express.Router(); +const { v7: uuidv7 } = require("uuid"); +const { valkey, track } = require("../valkey"); + +// Lua script: atomic check-and-reserve (prevents overselling under concurrency) +const RESERVE_SCRIPT = ` +local current = tonumber(redis.call('JSON.GET', KEYS[1], '$.inventory.quantity')) +local reserved = tonumber(redis.call('JSON.GET', KEYS[1], '$.inventory.reserved')) +if current == nil then return -1 end +local available = current - (reserved or 0) +if available >= tonumber(ARGV[1]) then + redis.call('JSON.NUMINCRBY', KEYS[1], '$.inventory.reserved', ARGV[1]) + return available - tonumber(ARGV[1]) +end +return -2 +`; + +// GET /api/inventory/:productId +router.get("/:productId", async (req, res) => { + const { productId } = req.params; + const key = productId.startsWith("product:") ? productId : `product:${productId}`; + + try { + const raw = await valkey.call("JSON.GET", key, "$.inventory"); + track("inventoryOps"); + + if (!raw) return res.status(404).json({ error: "Product not found" }); + + const inv = JSON.parse(raw)[0]; + const available = inv.quantity - (inv.reserved || 0); + + res.json({ + productId: key, + ...inv, + available, + inStock: available > 0, + stockLevel: available > 20 ? "high" : available > 5 ? "low" : available > 0 ? "critical" : "out", + valkeyOp: "JSON.GET $.inventory", + storedIn: "Valkey JSON", + }); + } catch (err) { + res.status(500).json({ error: "inventory_error", message: err.message }); + } +}); + +// POST /api/inventory/reserve – atomically reserve stock for checkout +router.post("/reserve", async (req, res) => { + const { productId, quantity = 1, orderId } = req.body; + if (!productId) return res.status(400).json({ error: "productId required" }); + + const key = productId.startsWith("product:") ? productId : `product:${productId}`; + const oid = orderId || `order:${uuidv7()}`; + + try { + const result = await valkey.eval(RESERVE_SCRIPT, 1, key, quantity); + track("inventoryOps"); + + if (result === -1) return res.status(404).json({ error: "Product not found" }); + if (result === -2) return res.status(409).json({ error: "Insufficient stock", available: 0 }); + + // Set reservation TTL (auto-release after 10 min) + await valkey.set(`reservation:${oid}:${key}`, quantity, "EX", 600); + + res.json({ + success: true, + orderId: oid, + productId: key, + quantity, + availableAfterReserve: result, + reservationExpiresIn: "10 minutes", + valkeyOp: "EVAL Lua (atomic reserve) + SET reservation EX 600", + storedIn: "Valkey JSON (inventory) + String (reservation TTL)", + }); + } catch (err) { + res.status(500).json({ error: "reserve_error", message: err.message }); + } +}); + +// POST /api/inventory/commit – confirm purchase, permanently decrement stock +router.post("/commit", async (req, res) => { + const { productId, quantity, orderId } = req.body; + if (!productId || !orderId) return res.status(400).json({ error: "productId and orderId required" }); + + const key = productId.startsWith("product:") ? productId : `product:${productId}`; + + try { + // Decrement quantity and reserved + await valkey.call("JSON.NUMINCRBY", key, "$.inventory.quantity", -quantity); + await valkey.call("JSON.NUMINCRBY", key, "$.inventory.reserved", -quantity); + await valkey.del(`reservation:${orderId}:${key}`); + track("inventoryOps"); + + res.json({ + success: true, + productId: key, + decremented: quantity, + valkeyOp: "JSON.NUMINCRBY (quantity) + JSON.NUMINCRBY (reserved) + DEL reservation", + storedIn: "Valkey JSON", + }); + } catch (err) { + res.status(500).json({ error: "commit_error", message: err.message }); + } +}); + +// POST /api/inventory/release – release reservation (cancel / timeout) +router.post("/release", async (req, res) => { + const { productId, quantity, orderId } = req.body; + if (!productId) return res.status(400).json({ error: "productId required" }); + + const key = productId.startsWith("product:") ? productId : `product:${productId}`; + + try { + await valkey.call("JSON.NUMINCRBY", key, "$.inventory.reserved", -quantity); + if (orderId) await valkey.del(`reservation:${orderId}:${key}`); + track("inventoryOps"); + + res.json({ + success: true, + released: quantity, + valkeyOp: "JSON.NUMINCRBY (reserved) -N", + storedIn: "Valkey JSON", + }); + } catch (err) { + res.status(500).json({ error: "release_error", message: err.message }); + } +}); + +// GET /api/inventory/all – snapshot of all product stock levels +router.get("/", async (req, res) => { + try { + const keys = await valkey.keys("product:*"); + const items = []; + + for (const key of keys) { + try { + const nameRaw = await valkey.call("JSON.GET", key, "$.name"); + const invRaw = await valkey.call("JSON.GET", key, "$.inventory"); + if (invRaw) { + const inv = JSON.parse(invRaw)[0]; + items.push({ + productId: key, + name: nameRaw ? JSON.parse(nameRaw)[0] : key, + quantity: inv.quantity, + reserved: inv.reserved || 0, + available: inv.quantity - (inv.reserved || 0), + }); + } + } catch (_) {} + } + + track("inventoryOps"); + res.json({ inventory: items, count: items.length, valkeyOp: "JSON.GET ($.inventory per product)", storedIn: "Valkey JSON" }); + } catch (err) { + res.status(500).json({ error: "inventory_all_error", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/products.js b/submissions/valkey-ecommerce-demo/backend/src/routes/products.js new file mode 100644 index 0000000..f539c77 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/products.js @@ -0,0 +1,230 @@ +/** + * Product Catalog – Valkey JSON + Sorted Sets + * + * Primary storage: JSON.SET {productId} $ → full product document + * Category index: ZADD category_products:{categoryId} {score} {productId} + * Price index: ZADD price_index {pricePaise} {productId} + * Brand index: SADD brand_products:{brand} {productId} + * + * Cache layer: product list is cached for 60s to reduce repeated JSON.GET scans. + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, track } = require("../valkey"); + +const PRODUCT_CACHE_TTL = 60; // seconds + +// Helper to resolve parent categories recursively to get all child category IDs (including the parent category ID itself) +async function getCategoryAndChildren(valkeyClient, categoryId) { + try { + const keys = await valkeyClient.keys("category:*"); + const categoryMap = {}; + for (const k of keys) { + const raw = await valkeyClient.call("JSON.GET", k); + if (raw) { + const catObj = JSON.parse(raw); + categoryMap[catObj.id] = catObj; + } + } + + const result = new Set([categoryId]); + const queue = [categoryId]; + while (queue.length > 0) { + const current = queue.shift(); + for (const id in categoryMap) { + if (categoryMap[id].parentId === current && !result.has(id)) { + result.add(id); + queue.push(id); + } + } + } + return Array.from(result); + } catch (err) { + console.error("[Helper] getCategoryAndChildren error:", err); + return [categoryId]; + } +} + +// GET /api/products +router.get("/", async (req, res) => { + const { category, brand, minPrice, maxPrice, page = 1, limit = 12 } = req.query; + + const cacheKey = `cache:products:${JSON.stringify(req.query)}`; + + // Try cache first + try { + const cached = await valkey.get(cacheKey); + if (cached) { + track("cacheHits"); + return res.json({ ...JSON.parse(cached), fromCache: true, valkeyOp: "GET (cache hit)", storedIn: "Valkey String cache" }); + } + track("cacheMisses"); + } catch (_) {} + + try { + // Scan all product keys (in production use FT.SEARCH index) + const keys = await valkey.keys("product:*"); + let products = []; + + for (const key of keys) { + try { + const raw = await valkey.call("JSON.GET", key); + if (raw) products.push(JSON.parse(raw)); + } catch (_) {} + } + + // Apply filters + if (category) { + const allowedCategories = await getCategoryAndChildren(valkey, category); + const allowedSet = new Set(allowedCategories); + products = products.filter((p) => allowedSet.has(p.categoryId)); + } + if (brand) products = products.filter((p) => p.brand?.toLowerCase() === brand.toLowerCase()); + if (minPrice) products = products.filter((p) => p.price?.amount >= Number(minPrice)); + if (maxPrice) products = products.filter((p) => p.price?.amount <= Number(maxPrice)); + + const total = products.length; + const offset = (Number(page) - 1) * Number(limit); + const paginated = products.slice(offset, offset + Number(limit)); + + const result = { + total, + page: Number(page), + pageSize: Number(limit), + products: paginated, + fromCache: false, + valkeyOp: "JSON.GET (per product)", + storedIn: "Valkey JSON documents", + }; + + // Cache the result + await valkey.setex(cacheKey, PRODUCT_CACHE_TTL, JSON.stringify(result)); + track("cacheHits"); // tracked as future cache + + res.json(result); + } catch (err) { + res.status(500).json({ error: "products_error", message: err.message }); + } +}); + +// GET /api/products/:id +router.get("/:id", async (req, res) => { + const productId = req.params.id.startsWith("product:") ? req.params.id : `product:${req.params.id}`; + + try { + const raw = await valkey.call("JSON.GET", productId); + track("cacheHits"); + + if (!raw) return res.status(404).json({ error: "Product not found" }); + + const product = JSON.parse(raw); + res.json({ ...product, valkeyOp: "JSON.GET", storedIn: "Valkey JSON" }); + } catch (err) { + res.status(500).json({ error: "product_error", message: err.message }); + } +}); + +// POST /api/products – create / upsert product +router.post("/", async (req, res) => { + const product = req.body; + if (!product.id || !product.name) { + return res.status(400).json({ error: "id and name required" }); + } + + try { + await valkey.call("JSON.SET", product.id, "$", JSON.stringify(product)); + + // Update category index + if (product.categoryId) { + const score = new Date(product.createdAt || Date.now()).getTime(); + await valkey.zadd(`category_products:${product.categoryId}`, score, product.id); + } + + // Update price index + if (product.price?.amount) { + await valkey.zadd("price_index", product.price.amount, product.id); + } + + // Update brand index + if (product.brand) { + await valkey.sadd(`brand_products:${product.brand.toLowerCase()}`, product.id); + } + + // Add to autocomplete suggestions + try { + await valkey.call("FT.SUGADD", "autocomplete", product.name, 100); + } catch (_) {} + + res.status(201).json({ + success: true, + id: product.id, + valkeyOp: "JSON.SET + ZADD + SADD", + storedIn: "Valkey JSON + Sorted Set + Set", + }); + } catch (err) { + res.status(500).json({ error: "product_create_error", message: err.message }); + } +}); + +// PATCH /api/products/:id – partial update +router.patch("/:id", async (req, res) => { + const productId = req.params.id.startsWith("product:") ? req.params.id : `product:${req.params.id}`; + const updates = req.body; + + try { + for (const [path, value] of Object.entries(updates)) { + await valkey.call("JSON.SET", productId, `$.${path}`, JSON.stringify(value)); + } + track("inventoryOps"); + res.json({ success: true, valkeyOp: "JSON.SET (partial)", storedIn: "Valkey JSON" }); + } catch (err) { + res.status(500).json({ error: "product_update_error", message: err.message }); + } +}); + +// GET /api/products/:id/inventory – real-time stock +router.get("/:id/inventory", async (req, res) => { + const productId = req.params.id.startsWith("product:") ? req.params.id : `product:${req.params.id}`; + + try { + const quantityRaw = await valkey.call("JSON.GET", productId, "$.inventory.quantity"); + const reservedRaw = await valkey.call("JSON.GET", productId, "$.inventory.reserved"); + track("inventoryOps"); + + const quantity = quantityRaw ? JSON.parse(quantityRaw)[0] : 0; + const reserved = reservedRaw ? JSON.parse(reservedRaw)[0] : 0; + const available = quantity - reserved; + + res.json({ + productId, + quantity, + reserved, + available, + inStock: available > 0, + valkeyOp: "JSON.GET ($.inventory.quantity)", + storedIn: "Valkey JSON", + }); + } catch (err) { + res.status(500).json({ error: "inventory_error", message: err.message }); + } +}); + +// GET /api/categories +router.get("/categories/all", async (req, res) => { + try { + const keys = await valkey.keys("category:*"); + const categories = []; + for (const key of keys) { + try { + const raw = await valkey.call("JSON.GET", key); + if (raw) categories.push(JSON.parse(raw)); + } catch (_) {} + } + res.json({ categories, valkeyOp: "JSON.GET (categories)", storedIn: "Valkey JSON" }); + } catch (err) { + res.status(500).json({ error: "categories_error", message: err.message }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/search.js b/submissions/valkey-ecommerce-demo/backend/src/routes/search.js new file mode 100644 index 0000000..dfb7945 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/search.js @@ -0,0 +1,368 @@ +/** + * Product Search – Valkey Search (FT.SEARCH) + Autocomplete (FT.SUGGET) + * + * Index: idx:products ON JSON PREFIX 1 product: + * Valkey commands: + * FT.CREATE – create index (on seed) + * FT.SEARCH – full-text search with filters + * FT.SUGADD – add autocomplete suggestions + * FT.SUGGET – get autocomplete suggestions + * FT.AGGREGATE – faceted aggregation + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, track } = require("../valkey"); + +const INDEX_NAME = "idx:products"; + +// Initialize search index (called on server start) +async function createSearchIndex() { + try { + await valkey.call( + "FT.CREATE", INDEX_NAME, + "ON", "JSON", + "PREFIX", "1", "product:", + "SCHEMA", + "$.name", "AS", "name", "TEXT", "SORTABLE", + "$.description", "AS", "description", "TEXT", + "$.brand", "AS", "brand", "TEXT", "SORTABLE", + "$.categoryId", "AS", "categoryId", "TAG", + "$.price.amount", "AS", "price", "NUMERIC", "SORTABLE", + "$.ratings.average", "AS", "rating", "NUMERIC", "SORTABLE", + "$.status", "AS", "status", "TAG" + ); + console.log("[Search] FT.CREATE idx:products – index created"); + } catch (err) { + if (err.message && err.message.includes("Index already exists")) { + console.log("[Search] idx:products already exists"); + } else { + console.error("[Search] FT.CREATE error:", err.message); + } + } +} + +// Helper to resolve parent categories recursively to get all child category IDs (including the parent category ID itself) +async function getCategoryAndChildren(valkeyClient, categoryId) { + try { + const keys = await valkeyClient.keys("category:*"); + const categoryMap = {}; + for (const k of keys) { + const raw = await valkeyClient.call("JSON.GET", k); + if (raw) { + const catObj = JSON.parse(raw); + categoryMap[catObj.id] = catObj; + } + } + + const result = new Set([categoryId]); + const queue = [categoryId]; + while (queue.length > 0) { + const current = queue.shift(); + for (const id in categoryMap) { + if (categoryMap[id].parentId === current && !result.has(id)) { + result.add(id); + queue.push(id); + } + } + } + return Array.from(result); + } catch (err) { + console.error("[Helper] getCategoryAndChildren error:", err); + return [categoryId]; + } +} + +// GET /api/search?q=&category=&minPrice=&maxPrice=&sort=&page=1 +router.get("/", async (req, res) => { + const { q = "*", category, minPrice, maxPrice, sort = "relevance", page = 1, limit = 12 } = req.query; + + let query = q === "*" || !q ? "*" : q; + const filters = []; + + if (category) { + const allowed = await getCategoryAndChildren(valkey, category); + const tagExpr = allowed.map((c) => c.replace(/:/g, "\\:")).join(" | "); + filters.push(`@categoryId:{${tagExpr}}`); + } + if (minPrice || maxPrice) { + const min = minPrice || "-inf"; + const max = maxPrice || "+inf"; + filters.push(`@price:[${min} ${max}]`); + } + + if (filters.length) query = `(${query}) ${filters.join(" ")}`; + + const sortArgs = []; + if (sort === "price_asc") sortArgs.push("SORTBY", "price", "ASC"); + else if (sort === "price_desc") sortArgs.push("SORTBY", "price", "DESC"); + else if (sort === "rating") sortArgs.push("SORTBY", "rating", "DESC"); + + const offset = (Number(page) - 1) * Number(limit); + + try { + const result = await valkey.call( + "FT.SEARCH", INDEX_NAME, query, + ...sortArgs, + "LIMIT", offset, Number(limit), + "RETURN", "6", "name", "price", "brand", "categoryId", "rating", "$.images[0].url" + ); + track("searchOps"); + + const total = result[0]; + const products = []; + for (let i = 1; i < result.length; i += 2) { + const id = result[i]; + const fields = result[i + 1]; + const obj = { id }; + for (let j = 0; j < fields.length; j += 2) { + let key = fields[j]; + if (key === "$.images[0].url") key = "image"; + obj[key] = fields[j + 1]; + } + products.push(obj); + } + + res.json({ + query: q, + total, + page: Number(page), + pageSize: Number(limit), + results: products, + valkeyOp: `FT.SEARCH idx:products "${query}"`, + storedIn: "Valkey Search Index (JSON documents)", + }); + } catch (err) { + // Fallback: scan all products + console.error("[Search] FT.SEARCH failed, falling back to scan:", err.message); + try { + const keys = await valkey.keys("product:*"); + const allProducts = []; + for (const k of keys) { + const raw = await valkey.call("JSON.GET", k); + if (raw) { + const p = JSON.parse(raw); + if (!q || q === "*" || p.name?.toLowerCase().includes(q.toLowerCase())) { + allProducts.push({ id: k, name: p.name, price: p.price?.amount, brand: p.brand }); + } + } + } + res.json({ query: q, total: allProducts.length, results: allProducts, fallback: true }); + } catch (e2) { + res.status(500).json({ error: "search_error", message: err.message }); + } + } +}); + +// GET /api/search/suggest?q= +router.get("/suggest", async (req, res) => { + const { q } = req.query; + if (!q) return res.json({ suggestions: [] }); + + try { + const result = await valkey.call("FT.SUGGET", "autocomplete", q, "FUZZY", "MAX", "8"); + track("searchOps"); + res.json({ + query: q, + suggestions: result || [], + valkeyOp: "FT.SUGGET autocomplete FUZZY", + storedIn: "Valkey Suggestion Index", + }); + } catch (err) { + res.json({ query: q, suggestions: [], error: err.message }); + } +}); + +// GET /api/search/facets?q= +router.get("/facets", async (req, res) => { + const { q = "*" } = req.query; + + try { + const brandFacets = await valkey.call( + "FT.AGGREGATE", INDEX_NAME, q, + "GROUPBY", "1", "@brand", + "REDUCE", "COUNT", "0", "AS", "count", + "SORTBY", "2", "@count", "DESC", + "LIMIT", "0", "10" + ); + track("searchOps"); + + const brands = []; + for (let i = 1; i < brandFacets.length; i++) { + const row = brandFacets[i]; + brands.push({ name: row[1], count: Number(row[3]) }); + } + + res.json({ + query: q, + facets: { brands }, + valkeyOp: "FT.AGGREGATE GROUPBY @brand", + storedIn: "Valkey Search Index", + }); + } catch (err) { + res.json({ query: q, facets: { brands: [] }, error: err.message }); + } +}); + +// GET /api/search/agentic?q=&userId= +router.get("/agentic", async (req, res) => { + const { q = "", userId } = req.query; + if (!q) return res.status(400).json({ error: "q (search query) is required" }); + + const valkeyOps = []; + + try { + // Step 1: Run FT.SEARCH for the query + let products = []; + let searchTotal = 0; + try { + const searchResult = await valkey.call( + "FT.SEARCH", INDEX_NAME, q, + "LIMIT", "0", "5", + "RETURN", "5", "name", "price", "brand", "categoryId", "$.images[0].url" + ); + track("searchOps"); + valkeyOps.push(`FT.SEARCH idx:products "${q}" LIMIT 0 5`); + + searchTotal = searchResult[0]; + for (let i = 1; i < searchResult.length; i += 2) { + const pid = searchResult[i]; + const fields = searchResult[i + 1]; + const obj = { id: pid }; + for (let j = 0; j < fields.length; j += 2) { + let key = fields[j]; + if (key === "$.images[0].url") key = "image"; + obj[key] = fields[j + 1]; + } + products.push(obj); + } + } catch (e) { + valkeyOps.push(`FT.SEARCH (error: ${e.message}) — falling back to scan`); + try { + const keys = await valkey.keys("product:*"); + for (const k of keys.slice(0, 5)) { + const raw = await valkey.call("JSON.GET", k); + if (raw) { + const p = JSON.parse(raw); + if (p.name?.toLowerCase().includes(q.toLowerCase())) { + products.push({ id: k, name: p.name, price: p.price?.amount, brand: p.brand }); + } + } + } + } catch (_) {} + } + + // Step 2: Get user affinities from Valkey + let userAffinities = []; + if (userId) { + try { + const affinityKey = `user_affinity:${userId}`; + const affinityRaw = await valkey.zrevrange(affinityKey, 0, 4, "WITHSCORES"); + valkeyOps.push(`ZREVRANGE ${affinityKey} 0 4 WITHSCORES`); + + for (let i = 0; i < affinityRaw.length; i += 2) { + const catId = affinityRaw[i]; + const score = parseFloat(affinityRaw[i + 1]); + // Try to get category name + let catName = catId; + try { + const catRaw = await valkey.call("JSON.GET", catId, "$.name"); + if (catRaw) { + const parsed = JSON.parse(catRaw); + catName = Array.isArray(parsed) ? parsed[0] : parsed; + } + } catch (_) {} + userAffinities.push({ categoryId: catId, name: catName, score }); + } + } catch (e) { + valkeyOps.push(`ZREVRANGE user_affinity (error: ${e.message})`); + } + } + + // Step 3: Build AI prompt and call Breeth API + const productList = products + .map((p) => `${p.name} (${p.brand || "Unknown Brand"}) - ₹${p.price || "N/A"}`) + .join(", "); + + const categoryInterests = userAffinities.length > 0 + ? userAffinities.map((a) => a.name).join(", ") + : "general shopping"; + + const aiPrompt = `User searched for '${q}'. Their top interests are ${categoryInterests}. Products found: ${productList || "No exact matches found"}. Give a helpful shopping recommendation in 2-3 sentences.`; + + let aiResponse = null; + let aiError = null; + try { + const https = require("https"); + const payload = JSON.stringify({ + query: aiPrompt, + context: { + searchQuery: q, + userInterests: categoryInterests, + productsFound: products.length, + }, + }); + + aiResponse = await new Promise((resolve, reject) => { + const options = { + hostname: "api.thebreeth.com", + path: "/v1/search", + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer ck_live_YhQWb-ZjIQxfs6AzDezC3zaPMPw4xJ8yRDkjIz7d16A", + "Content-Length": Buffer.byteLength(payload), + }, + }; + + const request = https.request(options, (resp) => { + let data = ""; + resp.on("data", (chunk) => { data += chunk; }); + resp.on("end", () => { + try { + const parsed = JSON.parse(data); + resolve(parsed.response || parsed.text || parsed.answer || parsed.message || JSON.stringify(parsed)); + } catch (_) { + resolve(data); + } + }); + }); + + request.on("error", reject); + request.setTimeout(5000, () => { + request.destroy(); + reject(new Error("Breeth API timeout")); + }); + request.write(payload); + request.end(); + }); + + valkeyOps.push("POST https://api.thebreeth.com/v1/search (Breeth AI)"); + } catch (e) { + aiError = e.message; + // Generate a helpful fallback response + if (products.length > 0) { + aiResponse = `Based on your search for "${q}", we found ${products.length} relevant product${products.length > 1 ? "s" : ""}. ${products[0]?.name} looks like a great option${products[0]?.price ? ` at ₹${products[0].price}` : ""}. ${userAffinities.length > 0 ? `Given your interest in ${userAffinities[0]?.name}, you might want to explore more in that category.` : "Browse our full catalog for more options."}`; + } else { + aiResponse = `We couldn't find exact matches for "${q}", but we have a wide range of products available. Try browsing by category or use filters to narrow your search. Our top categories include Electronics, Fashion, and Home Appliances.`; + } + valkeyOps.push(`Breeth AI (error: ${aiError}) — using fallback response`); + } + + res.json({ + query: q, + userId: userId || "guest", + searchTotal, + products, + aiResponse, + userAffinities, + valkeyOps, + storedIn: "Valkey Search Index (FT.SEARCH) + Valkey Sorted Set (user_affinity)", + ...(aiError ? { aiNote: "AI API unavailable, showing generated recommendation" } : {}), + }); + } catch (err) { + res.status(500).json({ error: "agentic_search_error", message: err.message }); + } +}); + +module.exports = { router, createSearchIndex }; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/stats.js b/submissions/valkey-ecommerce-demo/backend/src/routes/stats.js new file mode 100644 index 0000000..57349dc --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/stats.js @@ -0,0 +1,61 @@ +/** + * Valkey Stats Endpoint – for the real-time dashboard + * + * Returns live Valkey memory usage, key counts, and feature usage stats. + */ + +const express = require("express"); +const router = express.Router(); +const { valkey, getStats } = require("../valkey"); + +router.get("/", async (req, res) => { + try { + const info = await valkey.info("stats"); + const memory = await valkey.info("memory"); + const keyspace = await valkey.info("keyspace"); + + // Count keys per domain + const [cartKeys, sessionKeys, productKeys, couponKeys, conversationKeys, trendingKeys, wishlistKeys] = await Promise.all([ + valkey.keys("cart:*").then(k => k.length).catch(() => 0), + valkey.keys("session:*").then(k => k.length).catch(() => 0), + valkey.keys("product:*").then(k => k.length).catch(() => 0), + valkey.keys("coupon:*").then(k => k.length).catch(() => 0), + valkey.keys("conversation:*").then(k => k.length).catch(() => 0), + valkey.keys("trending:*").then(k => k.length).catch(() => 0), + valkey.keys("wishlist:*").then(k => k.length).catch(() => 0), + ]); + + // Parse memory used + const memMatch = memory.match(/used_memory_human:(\S+)/); + const usedMemory = memMatch ? memMatch[1] : "unknown"; + + res.json({ + status: "connected", + usedMemory, + featureStats: { + activeCarts: cartKeys, + activeSessions: sessionKeys, + cachedProducts: productKeys, + coupons: couponKeys, + aiConversations: conversationKeys, + trendingSets: trendingKeys, + wishlists: wishlistKeys, + }, + activeFeatures: [ + { name: "Cart Management", key: "HSET/HGETALL", description: "Valkey Hash", icon: "ShoppingCart", active: cartKeys > 0 }, + { name: "Session Storage", key: "SET session EX 86400", description: "Valkey String + TTL (24h)", icon: "Lock", active: true }, + { name: "Wishlist", key: "HSET/HDEL wishlist", description: "Valkey Hash", icon: "ShoppingCart", active: wishlistKeys > 0 }, + { name: "Product Catalog", key: "JSON.SET/GET", description: "Valkey JSON", icon: "Database", active: productKeys > 0 }, + { name: "Real-time Inventory", key: "JSON.NUMINCRBY + Lua", description: "Valkey JSON + Atomic Lua", icon: "Package", active: productKeys > 0 }, + { name: "AI Conversation Cache", key: "JSON.SET EX 1800", description: "Valkey JSON (30min TTL)", icon: "Robot", active: conversationKeys > 0 }, + { name: "Product Search", key: "FT.SEARCH idx:products", description: "Valkey Search Module", icon: "MagnifyingGlass", active: true }, + { name: "Trending Products", key: "ZINCRBY / ZREVRANGE", description: "Valkey Sorted Set + TTL", icon: "TrendUp", active: trendingKeys > 0 }, + ], + usageCounters: getStats(), + }); + } catch (err) { + res.status(500).json({ error: "stats_error", message: err.message, status: "disconnected" }); + } +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/routes/wishlist.js b/submissions/valkey-ecommerce-demo/backend/src/routes/wishlist.js new file mode 100644 index 0000000..7796a59 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/routes/wishlist.js @@ -0,0 +1,79 @@ +/** + * Wishlist – Valkey Hash (same pattern as cart) + * key: wishlist:{userId} + * HSET wishlist:{userId} {productId} {json} + * HGETALL / HDEL / DEL + */ +const express = require("express"); +const router = express.Router(); +const { valkey, track } = require("../valkey"); + +// GET /api/wishlist?userId= +router.get("/", async (req, res) => { + const { userId } = req.query; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `wishlist:${userId}`; + const raw = await valkey.hgetall(key); + track("cartOps"); + + const items = raw + ? Object.values(raw).map(v => { try { return JSON.parse(v); } catch { return null; } }).filter(Boolean) + : []; + + res.json({ + userId, + items, + itemCount: items.length, + valkeyOp: `HGETALL ${key}`, + storedIn: "Valkey Hash (wishlist)", + }); +}); + +// POST /api/wishlist/items +router.post("/items", async (req, res) => { + const { userId, productId, name, price, image } = req.body; + if (!userId || !productId) return res.status(400).json({ error: "userId and productId required" }); + + const key = `wishlist:${userId}`; + const item = { productId, name, price, image, addedAt: Date.now() }; + await valkey.hset(key, productId, JSON.stringify(item)); + track("cartOps"); + + res.json({ + success: true, + item, + valkeyOp: `HSET ${key} ${productId}`, + storedIn: "Valkey Hash (wishlist)", + }); +}); + +// DELETE /api/wishlist/items/:productId?userId= +router.delete("/items/:productId", async (req, res) => { + const { userId } = req.query; + const { productId } = req.params; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `wishlist:${userId}`; + await valkey.hdel(key, productId); + track("cartOps"); + + res.json({ + success: true, + valkeyOp: `HDEL ${key} ${productId}`, + storedIn: "Valkey Hash (wishlist)", + }); +}); + +// DELETE /api/wishlist?userId= — clear all +router.delete("/", async (req, res) => { + const { userId } = req.query; + if (!userId) return res.status(400).json({ error: "userId required" }); + + const key = `wishlist:${userId}`; + await valkey.del(key); + + res.json({ success: true, valkeyOp: `DEL ${key}` }); +}); + +module.exports = router; diff --git a/submissions/valkey-ecommerce-demo/backend/src/seed.js b/submissions/valkey-ecommerce-demo/backend/src/seed.js new file mode 100644 index 0000000..fe6843a --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/seed.js @@ -0,0 +1,836 @@ +/** + * Seeds Valkey with sample products, categories, vendors, and coupons + * following the HACKATHON.md data contracts. + * + * Run: node src/seed.js + */ + +const { valkey } = require("./valkey"); + +const PRODUCTS = [ + { + id: "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", + sku: "ELEC-PHN-SAM-001", + name: "Galaxy Ultra Pro 256GB", + slug: "galaxy-ultra-pro-256gb", + description: "Flagship smartphone with 200MP camera, 6.8 inch AMOLED display, and 5000mAh battery.", + shortDescription: "200MP camera, 6.8 AMOLED, 5000mAh", + categoryId: "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Samsung", + price: { amount: 89999, currency: "INR", compareAt: 99999 }, + images: [{ url: "https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=400&h=400&fit=crop&auto=format", alt: "Galaxy Ultra Pro", isPrimary: true }], + attributes: { color: "Phantom Black", storage: "256GB", ram: "12GB", display: "6.8 inch AMOLED", battery: "5000mAh", os: "Android 15" }, + tags: ["smartphone", "5g", "flagship", "camera", "samsung"], + inventory: { quantity: 150, reserved: 12, warehouse: "HYD-WH-01" }, + ratings: { average: 4.6, count: 2341 }, + status: "active", + createdAt: "2025-03-01T08:00:00Z", + updatedAt: "2025-05-18T12:30:00Z", + }, + { + id: "product:0192d4e6-3d5f-7b8c-9e0a-1b2c3d4e5f6a", + sku: "ELEC-LAP-DEL-001", + name: "Dell XPS 15 Laptop", + slug: "dell-xps-15", + description: "15.6 inch OLED display, Intel Core i9, 32GB RAM, 1TB SSD. Perfect for creators.", + shortDescription: "Core i9, 32GB RAM, OLED display", + categoryId: "category:0192d4e2-4c8d-7a2e-9f1b-3d5c7e8a0b4f", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Dell", + price: { amount: 149999, currency: "INR", compareAt: 179999 }, + images: [{ url: "https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=400&h=400&fit=crop&auto=format", alt: "Dell XPS 15", isPrimary: true }], + attributes: { processor: "Intel Core i9-13900H", ram: "32GB", storage: "1TB SSD", display: "15.6 inch OLED", os: "Windows 11 Pro" }, + tags: ["laptop", "creator", "gaming", "dell", "oled"], + inventory: { quantity: 45, reserved: 5, warehouse: "HYD-WH-01" }, + ratings: { average: 4.8, count: 892 }, + status: "active", + createdAt: "2025-02-15T08:00:00Z", + updatedAt: "2025-05-10T12:30:00Z", + }, + { + id: "product:0192d4e6-4e6a-7c9d-8f1b-2c3d4e5f6a7b", + sku: "FASH-TSH-NIK-001", + name: "Nike Air Max Sneakers", + slug: "nike-air-max-2025", + description: "Premium comfort sneakers with Air Max cushioning. Available in multiple colors.", + shortDescription: "Air Max cushioning, premium comfort", + categoryId: "category:0192d4e3-8c2d-7e5f-9b3a-0d4c6e7f1a2b", + vendorId: "vendor:0192d4e7-5e6f-7c8d-0f1a-2b3c4d5e6f7a", + brand: "Nike", + price: { amount: 12999, currency: "INR", compareAt: 15999 }, + images: [{ url: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=400&fit=crop&auto=format", alt: "Nike Air Max", isPrimary: true }], + attributes: { color: "White/Black", size: "UK 8", material: "Mesh upper, rubber sole" }, + tags: ["sneakers", "sports", "nike", "running", "fashion"], + inventory: { quantity: 200, reserved: 15, warehouse: "HYD-WH-02" }, + ratings: { average: 4.5, count: 3210 }, + status: "active", + createdAt: "2025-01-10T08:00:00Z", + updatedAt: "2025-05-20T10:00:00Z", + }, + { + id: "product:0192d4e6-5f7b-7d0e-8a2c-3d4e5f6a7b8c", + sku: "HOME-KIT-INS-001", + name: "Instant Pot Duo 7-in-1", + slug: "instant-pot-duo-7in1", + description: "7-in-1 multi-cooker. Pressure cooker, slow cooker, rice cooker, steamer, yogurt maker, and warmer.", + shortDescription: "7-in-1 electric pressure cooker", + categoryId: "category:0192d4e4-2b3c-7d4e-9f5a-6b7c8d9e0f1a", + vendorId: "vendor:0192d4e7-6f7a-7d8e-1a2b-3c4d5e6f7a8b", + brand: "Instant Pot", + price: { amount: 8499, currency: "INR", compareAt: 9999 }, + images: [{ url: "https://images.unsplash.com/photo-1585515320310-259814833e62?w=400&h=400&fit=crop&auto=format", alt: "Instant Pot", isPrimary: true }], + attributes: { capacity: "6 Quart", wattage: "1000W", modes: "7-in-1", color: "Silver" }, + tags: ["kitchen", "cooking", "home", "appliance", "pressure-cooker"], + inventory: { quantity: 80, reserved: 8, warehouse: "HYD-WH-01" }, + ratings: { average: 4.7, count: 5642 }, + status: "active", + createdAt: "2025-01-20T08:00:00Z", + updatedAt: "2025-04-15T12:00:00Z", + }, + { + id: "product:0192d4e6-6a8c-7e1f-9b3d-4e5f6a7b8c9d", + sku: "ELEC-TWS-SNY-001", + name: "Sony WF-1000XM5 Earbuds", + slug: "sony-wf-1000xm5", + description: "Industry-leading noise cancellation, 8hr battery life, Hi-Res Audio. Premium TWS earbuds.", + shortDescription: "Best-in-class ANC, 8hr battery", + categoryId: "category:0192d4e2-5d9e-7b3f-8a2c-4e6d1f9b0c5a", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Sony", + price: { amount: 24999, currency: "INR", compareAt: 29999 }, + images: [{ url: "https://images.unsplash.com/photo-1590658268037-6bf12165a8df?w=400&h=400&fit=crop&auto=format", alt: "Sony WF-1000XM5", isPrimary: true }], + attributes: { batteryLife: "8 hours + 16 hours case", noiseCancellation: "Industry-leading ANC", connectivity: "Bluetooth 5.3", color: "Black" }, + tags: ["earbuds", "tws", "sony", "anc", "audio"], + inventory: { quantity: 95, reserved: 20, warehouse: "HYD-WH-01" }, + ratings: { average: 4.9, count: 1876 }, + status: "active", + createdAt: "2025-02-01T08:00:00Z", + updatedAt: "2025-05-12T09:30:00Z", + }, + { + id: "product:0192d4e6-7b9d-7f2a-8c4e-5f6a7b8c9d0e", + sku: "SPRT-YGM-MAD-001", + name: "Adidas Yoga Mat Pro", + slug: "adidas-yoga-mat-pro", + description: "Extra thick 6mm mat with non-slip surface. Ideal for yoga, pilates, and home workouts.", + shortDescription: "6mm non-slip yoga mat", + categoryId: "category:0192d4e5-6f7a-7b8c-9d9e-0f1a2b3c4d5e", + vendorId: "vendor:0192d4e7-5e6f-7c8d-0f1a-2b3c4d5e6f7a", + brand: "Adidas", + price: { amount: 3299, currency: "INR", compareAt: 3999 }, + images: [{ url: "https://images.unsplash.com/photo-1601925260368-ae2f83cf8b7f?w=400&h=400&fit=crop&auto=format", alt: "Adidas Yoga Mat", isPrimary: true }], + attributes: { thickness: "6mm", material: "TPE", size: "183cm x 61cm", color: "Blue/Black" }, + tags: ["yoga", "fitness", "mat", "sports", "adidas"], + inventory: { quantity: 300, reserved: 25, warehouse: "HYD-WH-02" }, + ratings: { average: 4.4, count: 2109 }, + status: "active", + createdAt: "2025-01-05T08:00:00Z", + updatedAt: "2025-05-01T11:00:00Z", + }, + // --- 12 NEW PRODUCTS --- + { + id: "product:0192d5a1-1111-7a1b-8c1d-1e2f3a4b5c6d", + sku: "ELEC-PHN-APL-001", + name: "Apple iPhone 15 Pro", + slug: "apple-iphone-15-pro", + description: "Apple iPhone 15 Pro with A17 Pro chip, 48MP main camera, and titanium design for premium build quality. Features USB-C connectivity and Action Button for customizable shortcuts.", + shortDescription: "A17 Pro chip, 48MP camera, titanium design", + categoryId: "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Apple", + price: { amount: 134999, currency: "INR", compareAt: 149999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Apple iPhone 15 Pro", isPrimary: true }], + attributes: { processor: "A17 Pro", storage: "256GB", ram: "8GB", display: "6.1 inch Super Retina XDR", camera: "48MP Main + 12MP Ultra Wide", os: "iOS 17", color: "Natural Titanium" }, + tags: ["iphone", "apple", "5g", "smartphone", "flagship"], + inventory: { quantity: 120, reserved: 8, warehouse: "HYD-WH-01" }, + ratings: { average: 4.8, count: 3421 }, + status: "active", + createdAt: "2025-01-15T08:00:00Z", + updatedAt: "2025-05-20T10:00:00Z", + }, + { + id: "product:0192d5a1-2222-7b2c-9d2e-2f3a4b5c6d7e", + sku: "ELEC-LAP-APL-001", + name: "MacBook Air M3 13-inch", + slug: "macbook-air-m3-13inch", + description: "The MacBook Air with M3 chip delivers incredible performance and up to 18-hour battery life in a thin, fanless design. With a stunning Liquid Retina display and 8GB unified memory, it handles everything effortlessly.", + shortDescription: "M3 chip, 18hr battery, fanless design", + categoryId: "category:0192d4e2-4c8d-7a2e-9f1b-3d5c7e8a0b4f", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Apple", + price: { amount: 114999, currency: "INR", compareAt: 129999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "MacBook Air M3 13-inch", isPrimary: true }], + attributes: { processor: "Apple M3", ram: "8GB Unified", storage: "256GB SSD", display: "13.6 inch Liquid Retina", battery: "18 hours", os: "macOS Sonoma", color: "Midnight" }, + tags: ["macbook", "apple", "laptop", "m3", "ultrabook"], + inventory: { quantity: 60, reserved: 3, warehouse: "HYD-WH-01" }, + ratings: { average: 4.9, count: 1243 }, + status: "active", + createdAt: "2025-02-10T08:00:00Z", + updatedAt: "2025-05-15T12:00:00Z", + }, + { + id: "product:0192d5a1-3333-7c3d-0e3f-3a4b5c6d7e8f", + sku: "ELEC-PHN-OPL-001", + name: "OnePlus 12 5G", + slug: "oneplus-12-5g", + description: "OnePlus 12 5G is powered by Snapdragon 8 Gen 3 with a 50MP Hasselblad triple camera system. Featuring a 6.82 inch LTPO AMOLED display with 120Hz adaptive refresh rate and 100W SUPERVOOC fast charging.", + shortDescription: "Snapdragon 8 Gen 3, Hasselblad cameras, 100W charging", + categoryId: "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", + vendorId: "vendor:0192d4e7-6f7a-7d8e-1a2b-3c4d5e6f7a8b", + brand: "OnePlus", + price: { amount: 64999, currency: "INR", compareAt: 69999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "OnePlus 12 5G", isPrimary: true }], + attributes: { processor: "Snapdragon 8 Gen 3", storage: "256GB", ram: "12GB", display: "6.82 inch LTPO AMOLED 120Hz", camera: "50MP Hasselblad Triple", battery: "5400mAh", charging: "100W SUPERVOOC", os: "Android 14" }, + tags: ["oneplus", "5g", "smartphone", "hasselblad", "fast-charging"], + inventory: { quantity: 180, reserved: 22, warehouse: "HYD-WH-01" }, + ratings: { average: 4.7, count: 2891 }, + status: "active", + createdAt: "2025-01-20T08:00:00Z", + updatedAt: "2025-05-18T11:00:00Z", + }, + { + id: "product:0192d5a1-4444-7d4e-1f4a-4b5c6d7e8f9a", + sku: "ELEC-AUD-BOS-001", + name: "Bose QuietComfort 45", + slug: "bose-quietcomfort-45", + description: "The Bose QuietComfort 45 headphones deliver world-class noise cancellation with balanced audio performance. With up to 24 hours of battery life and comfortable over-ear design, they are perfect for travel and work.", + shortDescription: "World-class ANC, 24hr battery, over-ear comfort", + categoryId: "category:0192d4e2-5d9e-7b3f-8a2c-4e6d1f9b0c5a", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Bose", + price: { amount: 29990, currency: "INR", compareAt: 34990 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Bose QuietComfort 45", isPrimary: true }], + attributes: { type: "Over-Ear", noiseCancellation: "World-class ANC", batteryLife: "24 hours", connectivity: "Bluetooth 5.1", color: "White Smoke", foldable: true }, + tags: ["bose", "headphones", "anc", "audio", "wireless"], + inventory: { quantity: 75, reserved: 10, warehouse: "HYD-WH-01" }, + ratings: { average: 4.7, count: 1567 }, + status: "active", + createdAt: "2025-02-05T08:00:00Z", + updatedAt: "2025-05-10T09:00:00Z", + }, + { + id: "product:0192d5a1-5555-7e5f-2a5b-5c6d7e8f9a0b", + sku: "ELEC-WCH-APL-001", + name: "Apple Watch Series 9 GPS", + slug: "apple-watch-series-9-gps", + description: "Apple Watch Series 9 features the powerful S9 chip with Double Tap gesture and a brighter Always-On Retina display. Track your health with advanced sensors including ECG, blood oxygen, and crash detection.", + shortDescription: "S9 chip, Double Tap, health tracking, Always-On display", + categoryId: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Apple", + price: { amount: 41900, currency: "INR", compareAt: 45900 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Apple Watch Series 9 GPS", isPrimary: true }], + attributes: { chip: "S9", display: "Always-On Retina LTPO OLED", sensors: "ECG, Blood Oxygen, Temperature", gps: true, waterResistance: "WR50", batteryLife: "18 hours", compatibility: "iPhone" }, + tags: ["apple", "smartwatch", "health", "fitness", "wearable"], + inventory: { quantity: 90, reserved: 15, warehouse: "HYD-WH-01" }, + ratings: { average: 4.8, count: 2103 }, + status: "active", + createdAt: "2025-01-25T08:00:00Z", + updatedAt: "2025-05-12T14:00:00Z", + }, + { + id: "product:0192d5a1-6666-7f6a-3b6c-6d7e8f9a0b1c", + sku: "HOME-VAC-DYS-001", + name: "Dyson V11 Cordless Vacuum", + slug: "dyson-v11-cordless-vacuum", + description: "The Dyson V11 cordless vacuum features a powerful digital motor with up to 60 minutes of fade-free suction. Intelligent suction automatically adapts to the floor type detected by the machine.", + shortDescription: "60min battery, intelligent suction, cordless", + categoryId: "category:0192d4e4-2b3c-7d4e-9f5a-6b7c8d9e0f1a", + vendorId: "vendor:0192d4e7-6f7a-7d8e-1a2b-3c4d5e6f7a8b", + brand: "Dyson", + price: { amount: 44900, currency: "INR", compareAt: 52900 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Dyson V11 Cordless Vacuum", isPrimary: true }], + attributes: { motor: "Dyson Digital Motor V11", batteryLife: "60 minutes", suction: "185AW", bin: "0.76L", filtration: "Advanced whole-machine filtration", color: "Blue/Nickel" }, + tags: ["dyson", "vacuum", "cordless", "appliance", "cleaning"], + inventory: { quantity: 40, reserved: 5, warehouse: "HYD-WH-01" }, + ratings: { average: 4.6, count: 876 }, + status: "active", + createdAt: "2025-03-05T08:00:00Z", + updatedAt: "2025-05-14T10:30:00Z", + }, + { + id: "product:0192d5a1-7777-7a7b-4c7d-7e8f9a0b1c2d", + sku: "FASH-JNS-LEV-001", + name: "Levi's 501 Original Jeans", + slug: "levis-501-original-jeans", + description: "The iconic Levi's 501 Original Jeans with straight fit and button fly, a timeless classic since 1873. Made from 100% cotton denim for authentic comfort and durability that only gets better with wear.", + shortDescription: "Iconic straight fit, button fly, 100% cotton denim", + categoryId: "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a", + vendorId: "vendor:0192d4e7-5e6f-7c8d-0f1a-2b3c4d5e6f7a", + brand: "Levi's", + price: { amount: 3999, currency: "INR", compareAt: 5499 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Levi's 501 Original Jeans", isPrimary: true }], + attributes: { fit: "Straight", closure: "Button Fly", material: "100% Cotton Denim", rise: "Mid Rise", wash: "Medium Stonewash", size: "32x32" }, + tags: ["levis", "jeans", "fashion", "denim", "classic"], + inventory: { quantity: 500, reserved: 45, warehouse: "HYD-WH-02" }, + ratings: { average: 4.5, count: 6789 }, + status: "active", + createdAt: "2025-01-08T08:00:00Z", + updatedAt: "2025-05-05T09:00:00Z", + }, + { + id: "product:0192d5a1-8888-7b8c-5d8e-8f9a0b1c2d3e", + sku: "FASH-SNG-RYB-001", + name: "Ray-Ban Aviator Classic Sunglasses", + slug: "rayban-aviator-classic", + description: "The Ray-Ban Aviator Classic sunglasses with crystal lenses provide 100% UV protection and exceptional optical clarity. Originally designed for US military aviators, these iconic frames have become a timeless fashion staple.", + shortDescription: "Crystal lenses, 100% UV protection, iconic aviator style", + categoryId: "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a", + vendorId: "vendor:0192d4e7-5e6f-7c8d-0f1a-2b3c4d5e6f7a", + brand: "Ray-Ban", + price: { amount: 12999, currency: "INR", compareAt: 15500 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Ray-Ban Aviator Classic Sunglasses", isPrimary: true }], + attributes: { frameShape: "Aviator", lensType: "Crystal G-15", uvProtection: "100% UV400", frameMaterial: "Metal", color: "Gold/Green Classic", size: "58mm" }, + tags: ["rayban", "sunglasses", "aviator", "fashion", "uv-protection"], + inventory: { quantity: 200, reserved: 18, warehouse: "HYD-WH-02" }, + ratings: { average: 4.6, count: 3201 }, + status: "active", + createdAt: "2025-01-12T08:00:00Z", + updatedAt: "2025-05-08T11:00:00Z", + }, + { + id: "product:0192d5a1-9999-7c9d-6e9f-9a0b1c2d3e4f", + sku: "ELEC-AUD-JBL-001", + name: "JBL Charge 5 Bluetooth Speaker", + slug: "jbl-charge-5-bluetooth-speaker", + description: "The JBL Charge 5 delivers powerful JBL Pro Sound with a bold new design, featuring IP67 waterproof and dustproof rating. With a built-in power bank and 20-hour playtime, it keeps you charged up and partying all day.", + shortDescription: "IP67 waterproof, 20hr playtime, built-in power bank", + categoryId: "category:0192d4e2-5d9e-7b3f-8a2c-4e6d1f9b0c5a", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "JBL", + price: { amount: 14999, currency: "INR", compareAt: 17999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "JBL Charge 5 Bluetooth Speaker", isPrimary: true }], + attributes: { waterproofRating: "IP67", batteryLife: "20 hours", connectivity: "Bluetooth 5.1", powerBank: true, color: "Blue", weight: "960g" }, + tags: ["jbl", "speaker", "bluetooth", "waterproof", "audio"], + inventory: { quantity: 110, reserved: 12, warehouse: "HYD-WH-01" }, + ratings: { average: 4.7, count: 4532 }, + status: "active", + createdAt: "2025-02-20T08:00:00Z", + updatedAt: "2025-05-16T13:00:00Z", + }, + { + id: "product:0192d5a1-aaaa-7dae-7fab-ab0c1d2e3f4a", + sku: "ELEC-LAP-HPE-001", + name: "HP Spectre x360 14-inch", + slug: "hp-spectre-x360-14inch", + description: "The HP Spectre x360 14-inch is a premium 2-in-1 convertible laptop with OLED touch display and Intel Core Ultra processor. It features an elegant gem-cut design with OLED panel offering stunning colors and deep blacks.", + shortDescription: "2-in-1 convertible, OLED touch, Intel Core Ultra", + categoryId: "category:0192d4e2-4c8d-7a2e-9f1b-3d5c7e8a0b4f", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "HP", + price: { amount: 159990, currency: "INR", compareAt: 179990 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "HP Spectre x360 14-inch", isPrimary: true }], + attributes: { processor: "Intel Core Ultra 7", ram: "16GB LPDDR5", storage: "1TB SSD", display: "14 inch 2.8K OLED Touch 120Hz", convertible: true, os: "Windows 11 Home", color: "Nightfall Black" }, + tags: ["hp", "spectre", "laptop", "2-in-1", "oled"], + inventory: { quantity: 25, reserved: 2, warehouse: "HYD-WH-01" }, + ratings: { average: 4.7, count: 543 }, + status: "active", + createdAt: "2025-03-10T08:00:00Z", + updatedAt: "2025-05-18T15:00:00Z", + }, + { + id: "product:0192d5a1-bbbb-7ebf-8fbc-bc1d2e3f4a5b", + sku: "ELEC-FIT-FBT-001", + name: "Fitbit Charge 6 Fitness Tracker", + slug: "fitbit-charge-6-fitness-tracker", + description: "Fitbit Charge 6 features built-in GPS, heart rate monitoring, and Google Maps integration for comprehensive fitness tracking. With ECG app, skin temperature sensor, and stress management score, it helps you understand your overall health.", + shortDescription: "Built-in GPS, ECG, Google Maps, 7-day battery", + categoryId: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", + vendorId: "vendor:0192d4e7-6f7a-7d8e-1a2b-3c4d5e6f7a8b", + brand: "Fitbit", + price: { amount: 14999, currency: "INR", compareAt: 17999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "Fitbit Charge 6 Fitness Tracker", isPrimary: true }], + attributes: { gps: "Built-in GPS", heartRate: "Continuous 24/7", battery: "7 days", display: "AMOLED", waterResistance: "Water resistant 50m", googleIntegration: true, color: "Black" }, + tags: ["fitbit", "fitness", "tracker", "health", "wearable"], + inventory: { quantity: 150, reserved: 20, warehouse: "HYD-WH-01" }, + ratings: { average: 4.5, count: 2876 }, + status: "active", + createdAt: "2025-02-25T08:00:00Z", + updatedAt: "2025-05-17T10:00:00Z", + }, + { + id: "product:0192d5a1-cccc-7fcf-9acd-cd2e3f4a5b6c", + sku: "HOME-ROB-IRB-001", + name: "iRobot Roomba i7+ Robot Vacuum", + slug: "irobot-roomba-i7-plus", + description: "The iRobot Roomba i7+ automatically empties its bin into a Clean Base Automatic Dirt Disposal for hands-free cleaning for weeks. With Smart Mapping technology, it learns your home and you can clean by room with voice or app control.", + shortDescription: "Auto empty base, Smart Mapping, voice control compatible", + categoryId: "category:0192d4e4-2b3c-7d4e-9f5a-6b7c8d9e0f1a", + vendorId: "vendor:0192d4e7-6f7a-7d8e-1a2b-3c4d5e6f7a8b", + brand: "iRobot", + price: { amount: 59999, currency: "INR", compareAt: 69999 }, + images: [{ url: "/assets/images/thumbs/product-img1.png", alt: "iRobot Roomba i7+ Robot Vacuum", isPrimary: true }], + attributes: { autoEmpty: "Clean Base Automatic Dirt Disposal", mapping: "Smart Mapping with Imprint", suction: "10x Power-Lifting Suction", filters: "High-Efficiency Filter", voiceControl: "Alexa & Google Assistant", runtime: "75 minutes" }, + tags: ["irobot", "roomba", "robot-vacuum", "smart-home", "appliance"], + inventory: { quantity: 30, reserved: 4, warehouse: "HYD-WH-01" }, + ratings: { average: 4.4, count: 1234 }, + status: "active", + createdAt: "2025-03-15T08:00:00Z", + updatedAt: "2025-05-19T09:30:00Z", + }, + { + id: "product:potato-5kg", + sku: "GROC-VEG-POT-001", + name: "Organic Potatoes (5kg)", + slug: "organic-potatoes-5kg", + description: "Freshly harvested organic potatoes from local farms. Ideal for baking, mashing, or frying.", + shortDescription: "Fresh organic potatoes, 5kg pack", + categoryId: "category:vegetables-fruit", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Taylor Farms", + price: { amount: 150, currency: "INR", compareAt: 200 }, + images: [{ url: "https://images.unsplash.com/photo-1518977676601-b53f82aba655?w=400&h=400&fit=crop&auto=format", alt: "Potatoes", isPrimary: true }], + attributes: { origin: "Local Farm", weight: "5kg", type: "Organic" }, + tags: ["vegetables", "organic", "potatoes", "fresh"], + inventory: { quantity: 500, reserved: 0, warehouse: "HYD-WH-02" }, + ratings: { average: 4.5, count: 120 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:tomato-1kg", + sku: "GROC-VEG-TOM-001", + name: "Fresh Red Tomatoes (1kg)", + slug: "fresh-red-tomatoes-1kg", + description: "Ripe, juicy red tomatoes. Perfect for salads, curries, and sauces.", + shortDescription: "Ripe, juicy red tomatoes, 1kg pack", + categoryId: "category:vegetables-fruit", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Taylor Farms", + price: { amount: 60, currency: "INR", compareAt: 80 }, + images: [{ url: "https://images.unsplash.com/photo-1595855759920-86582396756a?w=400&h=400&fit=crop&auto=format", alt: "Tomatoes", isPrimary: true }], + attributes: { origin: "Local Farm", weight: "1kg", type: "Fresh" }, + tags: ["vegetables", "tomatoes", "fresh", "juicy"], + inventory: { quantity: 400, reserved: 5, warehouse: "HYD-WH-02" }, + ratings: { average: 4.4, count: 95 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:apple-6pcs", + sku: "GROC-FRU-APP-001", + name: "Organic Red Apples (6pcs)", + slug: "organic-red-apples-6pcs", + description: "Crisp and sweet organic red apples. Rich in fiber and antioxidants.", + shortDescription: "Sweet, crisp organic red apples", + categoryId: "category:vegetables-fruit", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Taylor Farms", + price: { amount: 180, currency: "INR", compareAt: 220 }, + images: [{ url: "https://images.unsplash.com/photo-1560806887-1e4cd0b6cbd6?w=400&h=400&fit=crop&auto=format", alt: "Apples", isPrimary: true }], + attributes: { origin: "Shimla", count: "6 pcs", type: "Organic" }, + tags: ["fruits", "apples", "fresh", "organic"], + inventory: { quantity: 200, reserved: 2, warehouse: "HYD-WH-02" }, + ratings: { average: 4.6, count: 180 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:coke-250ml", + sku: "GROC-BEV-COK-001", + name: "Coca-Cola Soft Drink (250ml Can)", + slug: "coca-cola-250ml", + description: "Refreshing Coca-Cola classic soft drink. Best served chilled.", + shortDescription: "Refreshing classic cola drink", + categoryId: "category:beverages", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Coca-Cola", + price: { amount: 40, currency: "INR", compareAt: 40 }, + images: [{ url: "https://images.unsplash.com/photo-1622483767028-3f66f32aef97?w=400&h=400&fit=crop&auto=format", alt: "Coke", isPrimary: true }], + attributes: { volume: "250ml", packaging: "Can" }, + tags: ["beverages", "cola", "soda", "cold-drink"], + inventory: { quantity: 1000, reserved: 0, warehouse: "HYD-WH-02" }, + ratings: { average: 4.7, count: 1200 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:orangejuice-1l", + sku: "GROC-BEV-ORJ-001", + name: "100% Pure Orange Juice (1L)", + slug: "pure-orange-juice-1l", + description: "No added sugar, 100% pure orange juice packed with Vitamin C.", + shortDescription: "100% pure orange juice, 1L pack", + categoryId: "category:beverages", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Tropicana", + price: { amount: 120, currency: "INR", compareAt: 140 }, + images: [{ url: "https://images.unsplash.com/photo-1613478223719-2ab802602423?w=400&h=400&fit=crop&auto=format", alt: "Orange Juice", isPrimary: true }], + attributes: { volume: "1L", type: "100% Juice", packaging: "Tetrapack" }, + tags: ["beverages", "juice", "orange", "healthy"], + inventory: { quantity: 300, reserved: 4, warehouse: "HYD-WH-02" }, + ratings: { average: 4.5, count: 220 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:chicken-breast-1kg", + sku: "GROC-MEA-CHK-001", + name: "Fresh Chicken Breast (1kg)", + slug: "fresh-chicken-breast-1kg", + description: "Premium skinless, boneless chicken breast. Cleaned and ready for cooking.", + shortDescription: "Skinless, boneless chicken breast", + categoryId: "category:meats-seafood", + vendorId: "vendor:0192d4e7-5e6f-7c8d-0f1a-2b3c4d5e6f7a", + brand: "FreshMeat", + price: { amount: 280, currency: "INR", compareAt: 320 }, + images: [{ url: "https://images.unsplash.com/photo-1604503468506-a8da13d82791?w=400&h=400&fit=crop&auto=format", alt: "Chicken Breast", isPrimary: true }], + attributes: { weight: "1kg", status: "Chilled" }, + tags: ["meats", "chicken", "fresh", "protein"], + inventory: { quantity: 150, reserved: 2, warehouse: "HYD-WH-02" }, + ratings: { average: 4.6, count: 140 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:milk-1l", + sku: "GROC-DAI-MLK-001", + name: "Fresh Whole Milk (1L)", + slug: "fresh-whole-milk-1l", + description: "Pasteurized fresh whole milk with high cream content. Direct from local dairy farms.", + shortDescription: "Fresh pasteurized whole milk, 1L", + categoryId: "category:breakfast-dairy", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Amul", + price: { amount: 66, currency: "INR", compareAt: 70 }, + images: [{ url: "https://images.unsplash.com/photo-1563636619-e9143da7973b?w=400&h=400&fit=crop&auto=format", alt: "Milk", isPrimary: true }], + attributes: { volume: "1L", type: "Whole Milk", fatContent: "4.5%" }, + tags: ["dairy", "milk", "breakfast", "fresh"], + inventory: { quantity: 600, reserved: 10, warehouse: "HYD-WH-02" }, + ratings: { average: 4.8, count: 540 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:eggs-12pcs", + sku: "GROC-DAI-EGG-001", + name: "Organic White Eggs (12pcs)", + slug: "organic-white-eggs-12pcs", + description: "Farm-fresh organic white eggs. Packed with protein and essential nutrients.", + shortDescription: "Farm-fresh organic white eggs", + categoryId: "category:breakfast-dairy", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Eggo", + price: { amount: 90, currency: "INR", compareAt: 110 }, + images: [{ url: "https://images.unsplash.com/photo-1506976785307-8732e854ad03?w=400&h=400&fit=crop&auto=format", alt: "Eggs", isPrimary: true }], + attributes: { count: "12 pcs", type: "Organic" }, + tags: ["dairy", "eggs", "breakfast", "protein"], + inventory: { quantity: 300, reserved: 5, warehouse: "HYD-WH-02" }, + ratings: { average: 4.7, count: 320 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:frenchfries-1kg", + sku: "GROC-FRO-FRY-001", + name: "Frozen French Fries (1kg)", + slug: "frozen-french-fries-1kg", + description: "Crispy frozen French fries. Ready to deep fry or air fry.", + shortDescription: "Crispy frozen French fries, 1kg pack", + categoryId: "category:frozen-foods", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "McCain", + price: { amount: 180, currency: "INR", compareAt: 220 }, + images: [{ url: "https://images.unsplash.com/photo-1576107232684-1279f390859f?w=400&h=400&fit=crop&auto=format", alt: "French Fries", isPrimary: true }], + attributes: { weight: "1kg", storage: "Keep Frozen" }, + tags: ["frozen", "fries", "potato", "snacks"], + inventory: { quantity: 200, reserved: 4, warehouse: "HYD-WH-02" }, + ratings: { average: 4.5, count: 180 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:cookies-200g", + sku: "GROC-SNK-COK-001", + name: "Chocolate Chip Cookies (200g)", + slug: "chocolate-chip-cookies-200g", + description: "Freshly baked cookies loaded with rich chocolate chips. Perfect snack with tea or milk.", + shortDescription: "Rich chocolate chip cookies", + categoryId: "category:biscuits-snacks", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "GoodDay", + price: { amount: 45, currency: "INR", compareAt: 50 }, + images: [{ url: "https://images.unsplash.com/photo-1499636136210-6f4ee915583e?w=400&h=400&fit=crop&auto=format", alt: "Cookies", isPrimary: true }], + attributes: { weight: "200g", flavor: "Chocolate" }, + tags: ["snacks", "biscuits", "cookies", "chocolate"], + inventory: { quantity: 500, reserved: 8, warehouse: "HYD-WH-02" }, + ratings: { average: 4.6, count: 280 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + { + id: "product:rice-5kg", + sku: "GROC-STA-RIC-001", + name: "Organic Basmati Rice (5kg)", + slug: "organic-basmati-rice-5kg", + description: "Premium extra-long grain aromatic organic Basmati rice. Aged to perfection.", + shortDescription: "Aromatic extra-long grain Basmati rice", + categoryId: "category:grocery-staples", + vendorId: "vendor:0192d4e7-4d5e-7b7c-9e9f-1a2b3c4d5e6f", + brand: "Daawat", + price: { amount: 650, currency: "INR", compareAt: 750 }, + images: [{ url: "https://images.unsplash.com/photo-1586201375761-83865001e31c?w=400&h=400&fit=crop&auto=format", alt: "Basmati Rice", isPrimary: true }], + attributes: { weight: "5kg", type: "Basmati Rice", grain: "Extra Long" }, + tags: ["staples", "rice", "basmati", "organic"], + inventory: { quantity: 150, reserved: 2, warehouse: "HYD-WH-02" }, + ratings: { average: 4.7, count: 350 }, + status: "active", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } +]; + +const CATEGORIES = [ + { id: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b", name: "Electronics", slug: "electronics", icon: "desktop", parentId: null }, + { id: "category:0192d4e2-3a7b-7e1f-8c4d-2b6a9f0e5d7c", name: "Smartphones", slug: "smartphones", icon: "device-mobile", parentId: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b" }, + { id: "category:0192d4e2-4c8d-7a2e-9f1b-3d5c7e8a0b4f", name: "Laptops", slug: "laptops", icon: "laptop", parentId: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b" }, + { id: "category:0192d4e2-5d9e-7b3f-8a2c-4e6d1f9b0c5a", name: "Audio", slug: "audio", icon: "headphones", parentId: "category:0192d4e2-1f5a-7c3d-9b2e-8a4f6d0c1e3b" }, + { id: "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a", name: "Fashion", slug: "fashion", icon: "t-shirt", parentId: null }, + { id: "category:0192d4e3-8c2d-7e5f-9b3a-0d4c6e7f1a2b", name: "Footwear", slug: "footwear", icon: "sneaker", parentId: "category:0192d4e3-7b1c-7d4e-8a2f-9c3b5d6e0f1a" }, + { id: "category:0192d4e4-1a2b-7c3d-8e4f-5a6b7c8d9e0f", name: "Home & Kitchen", slug: "home-kitchen", icon: "house", parentId: null }, + { id: "category:0192d4e4-2b3c-7d4e-9f5a-6b7c8d9e0f1a", name: "Appliances", slug: "appliances", icon: "plug", parentId: "category:0192d4e4-1a2b-7c3d-8e4f-5a6b7c8d9e0f" }, + { id: "category:0192d4e5-5e6f-7a7b-8c8d-9e0f1a2b3c4d", name: "Sports & Outdoors", slug: "sports-outdoors", icon: "basketball", parentId: null }, + { id: "category:0192d4e5-6f7a-7b8c-9d9e-0f1a2b3c4d5e", name: "Fitness", slug: "fitness", icon: "barbell", parentId: "category:0192d4e5-5e6f-7a7b-8c8d-9e0f1a2b3c4d" }, + { id: "category:grocery", name: "Grocery", slug: "grocery", icon: "carrot", parentId: null }, + { id: "category:vegetables-fruit", name: "Vegetables & Fruit", slug: "vegetables-fruit", icon: "carrot", parentId: "category:grocery" }, + { id: "category:beverages", name: "Beverages", slug: "beverages", icon: "brandy", parentId: "category:grocery" }, + { id: "category:meats-seafood", name: "Meats & Seafood", slug: "meats-seafood", icon: "brandy", parentId: "category:grocery" }, + { id: "category:breakfast-dairy", name: "Breakfast & Dairy", slug: "breakfast-dairy", icon: "brandy", parentId: "category:grocery" }, + { id: "category:frozen-foods", name: "Frozen Foods", slug: "frozen-foods", icon: "brandy", parentId: "category:grocery" }, + { id: "category:biscuits-snacks", name: "Biscuits & Snacks", slug: "biscuits-snacks", icon: "brandy", parentId: "category:grocery" }, + { id: "category:grocery-staples", name: "Grocery & Staples", slug: "grocery-staples", icon: "brandy", parentId: "category:grocery" }, +]; + +const COUPONS = [ + { + code: "VALKEY10", + type: "percentage", + value: 10, + minOrderAmount: 5000, + maxDiscount: 10000, + validFrom: "2025-01-01T00:00:00Z", + validUntil: "2026-12-31T23:59:59Z", + usageLimit: 1000, + usedCount: 342, + applicableCategories: [], + active: true, + }, + { + code: "FLAT500", + type: "fixed", + value: 500, + minOrderAmount: 3000, + maxDiscount: 500, + validFrom: "2025-01-01T00:00:00Z", + validUntil: "2026-12-31T23:59:59Z", + usageLimit: 500, + usedCount: 123, + applicableCategories: [], + active: true, + }, + { + code: "BREETH20", + type: "percentage", + value: 20, + minOrderAmount: 10000, + maxDiscount: 5000, + validFrom: "2025-01-01T00:00:00Z", + validUntil: "2026-12-31T23:59:59Z", + usageLimit: 200, + usedCount: 45, + applicableCategories: [], + active: true, + }, +]; + +async function seed() { + console.log("[Seed] Connecting to Valkey..."); + await valkey.connect(); + + console.log("[Seed] Seeding categories..."); + for (const cat of CATEGORIES) { + await valkey.call("JSON.SET", cat.id, "$", JSON.stringify(cat)); + } + + console.log("[Seed] Seeding products..."); + for (const p of PRODUCTS) { + await valkey.call("JSON.SET", p.id, "$", JSON.stringify(p)); + + // Category index + if (p.categoryId) { + await valkey.zadd(`category_products:${p.categoryId}`, Date.now(), p.id); + } + + // Price index + if (p.price?.amount) { + await valkey.zadd("price_index", p.price.amount, p.id); + } + + // Brand index + if (p.brand) { + await valkey.sadd(`brand_products:${p.brand.toLowerCase()}`, p.id); + } + + // Autocomplete suggestions + try { + await valkey.call("FT.SUGADD", "autocomplete", p.name, 100); + } catch (_) {} + + // Seed some trending scores + await valkey.zincrby("trending:global:24h", Math.floor(Math.random() * 100), p.id); + await valkey.zincrby("trending:global:1h", Math.floor(Math.random() * 20), p.id); + } + await valkey.expire("trending:global:1h", 3600); + + console.log("[Seed] Seeding coupons..."); + for (const c of COUPONS) { + await valkey.call("JSON.SET", `coupon:${c.code}`, "$", JSON.stringify(c)); + } + + const bcrypt = require("bcryptjs"); + + // Demo user (keep existing) + const demoUserId = "user:demo-0192d4e0-7b3a-7f5c-9e1a-4b8c2d6f0a1e"; + const demoHash = await bcrypt.hash("demo1234", 12); + const demoUser = { + id: demoUserId, + email: "demo@valkey.io", + passwordHash: demoHash, + firstName: "Priya", + lastName: "Sharma", + role: "customer", + createdAt: new Date().toISOString(), + }; + await valkey.call("JSON.SET", demoUserId, "$", JSON.stringify(demoUser)); + await valkey.set("email:demo@valkey.io", demoUserId); + + // Demo cart + const cartItem = { name: "Sony WF-1000XM5 Earbuds", price: 24999, image: "/assets/images/thumbs/product-img5.png", quantity: 1 }; + await valkey.hset(`cart:${demoUserId}`, "product:0192d4e6-6a8c-7e1f-9b3d-4e5f6a7b8c9d", JSON.stringify(cartItem)); + await valkey.expire(`cart:${demoUserId}`, 604800); + + // Admin user + const adminUserId = "user:admin-0192d5b0-1111-7a1b-8c1d-admin00000001"; + const adminHash = await bcrypt.hash("admin1234", 12); + const adminUser = { + id: adminUserId, + email: "admin@valkey.io", + passwordHash: adminHash, + firstName: "Admin", + lastName: "User", + role: "admin", + createdAt: new Date().toISOString(), + }; + await valkey.call("JSON.SET", adminUserId, "$", JSON.stringify(adminUser)); + await valkey.set("email:admin@valkey.io", adminUserId); + + // Manager user + const managerUserId = "user:manager-0192d5b0-2222-7b2c-9d2e-manager000001"; + const managerHash = await bcrypt.hash("manager1234", 12); + const managerUser = { + id: managerUserId, + email: "manager@valkey.io", + passwordHash: managerHash, + firstName: "Manager", + lastName: "User", + role: "manager", + createdAt: new Date().toISOString(), + }; + await valkey.call("JSON.SET", managerUserId, "$", JSON.stringify(managerUser)); + await valkey.set("email:manager@valkey.io", managerUserId); + + // Regular demo users + const user1Id = "user:arjun-0192d5b0-3333-7c3d-0e3f-user000000001"; + const user1Hash = await bcrypt.hash("user1234", 12); + const user1 = { + id: user1Id, + email: "user1@demo.com", + passwordHash: user1Hash, + firstName: "Arjun", + lastName: "Sharma", + role: "customer", + createdAt: new Date().toISOString(), + }; + await valkey.call("JSON.SET", user1Id, "$", JSON.stringify(user1)); + await valkey.set("email:user1@demo.com", user1Id); + + const user2Id = "user:priya-0192d5b0-4444-7d4e-1f4a-user000000002"; + const user2Hash = await bcrypt.hash("user1234", 12); + const user2 = { + id: user2Id, + email: "user2@demo.com", + passwordHash: user2Hash, + firstName: "Priya", + lastName: "Patel", + role: "customer", + createdAt: new Date().toISOString(), + }; + await valkey.call("JSON.SET", user2Id, "$", JSON.stringify(user2)); + await valkey.set("email:user2@demo.com", user2Id); + + // Seed wishlists for demo users + console.log("[Seed] Seeding wishlists..."); + + // demo@valkey.io wishlist + const demoWishlistItems = [ + { productId: "product:0192d5a1-1111-7a1b-8c1d-1e2f3a4b5c6d", name: "Apple iPhone 15 Pro", price: 134999, image: "/assets/images/thumbs/product-img1.png" }, + { productId: "product:0192d5a1-2222-7b2c-9d2e-2f3a4b5c6d7e", name: "MacBook Air M3 13-inch", price: 114999, image: "/assets/images/thumbs/product-img1.png" }, + { productId: "product:0192d5a1-4444-7d4e-1f4a-4b5c6d7e8f9a", name: "Bose QuietComfort 45", price: 29990, image: "/assets/images/thumbs/product-img1.png" }, + ]; + for (const item of demoWishlistItems) { + await valkey.hset(`wishlist:${demoUserId}`, item.productId, JSON.stringify({ ...item, addedAt: Date.now() })); + } + + // user1@demo.com wishlist + const user1WishlistItems = [ + { productId: "product:0192d5a1-3333-7c3d-0e3f-3a4b5c6d7e8f", name: "OnePlus 12 5G", price: 64999, image: "/assets/images/thumbs/product-img1.png" }, + { productId: "product:0192d5a1-5555-7e5f-2a5b-5c6d7e8f9a0b", name: "Apple Watch Series 9 GPS", price: 41900, image: "/assets/images/thumbs/product-img1.png" }, + ]; + for (const item of user1WishlistItems) { + await valkey.hset(`wishlist:${user1Id}`, item.productId, JSON.stringify({ ...item, addedAt: Date.now() })); + } + + // user2@demo.com wishlist + const user2WishlistItems = [ + { productId: "product:0192d5a1-7777-7a7b-4c7d-7e8f9a0b1c2d", name: "Levi's 501 Original Jeans", price: 3999, image: "/assets/images/thumbs/product-img1.png" }, + { productId: "product:0192d5a1-9999-7c9d-6e9f-9a0b1c2d3e4f", name: "JBL Charge 5 Bluetooth Speaker", price: 14999, image: "/assets/images/thumbs/product-img1.png" }, + { productId: "product:0192d5a1-bbbb-7ebf-8fbc-bc1d2e3f4a5b", name: "Fitbit Charge 6 Fitness Tracker", price: 14999, image: "/assets/images/thumbs/product-img1.png" }, + ]; + for (const item of user2WishlistItems) { + await valkey.hset(`wishlist:${user2Id}`, item.productId, JSON.stringify({ ...item, addedAt: Date.now() })); + } + + // Seed comparison data for guest demo + console.log("[Seed] Seeding comparison data..."); + await valkey.sadd( + "compare:guest:demo", + "product:0192d4e6-2c4e-7a6b-8d8f-0a1b2c3d4e5f", + "product:0192d5a1-1111-7a1b-8c1d-1e2f3a4b5c6d" + ); + + console.log("\n[Seed] Done!"); + console.log("[Seed] Products seeded: " + PRODUCTS.length); + console.log("[Seed] Demo login: demo@valkey.io / demo1234"); + console.log("[Seed] Admin login: admin@valkey.io / admin1234"); + console.log("[Seed] Manager login: manager@valkey.io / manager1234"); + console.log("[Seed] User1 login: user1@demo.com / user1234"); + console.log("[Seed] User2 login: user2@demo.com / user1234"); + console.log("[Seed] Coupons: VALKEY10, FLAT500, BREETH20"); + process.exit(0); +} + +seed().catch((e) => { + console.error("[Seed] Error:", e.message); + process.exit(1); +}); diff --git a/submissions/valkey-ecommerce-demo/backend/src/valkey.js b/submissions/valkey-ecommerce-demo/backend/src/valkey.js new file mode 100644 index 0000000..b425477 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/backend/src/valkey.js @@ -0,0 +1,33 @@ +const Redis = require("ioredis"); + +const VALKEY_URL = process.env.VALKEY_URL || "redis://localhost:6379"; + +const valkey = new Redis(VALKEY_URL, { + lazyConnect: true, + retryStrategy: (times) => Math.min(times * 100, 3000), + maxRetriesPerRequest: 3, +}); + +valkey.on("connect", () => console.log("[Valkey] Connected to Valkey at", VALKEY_URL)); +valkey.on("error", (err) => console.error("[Valkey] Error:", err.message)); + +// Track which Valkey features are actively being used (for dashboard) +const usageStats = { + cartOps: 0, + sessionOps: 0, + cacheHits: 0, + cacheMisses: 0, + inventoryOps: 0, + searchOps: 0, + aiOps: 0, +}; + +function track(feature) { + if (usageStats[feature] !== undefined) usageStats[feature]++; +} + +function getStats() { + return { ...usageStats }; +} + +module.exports = { valkey, track, getStats }; diff --git a/submissions/valkey-ecommerce-demo/docker-compose.yml b/submissions/valkey-ecommerce-demo/docker-compose.yml new file mode 100644 index 0000000..93c57d3 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.9" + +services: + valkey: + image: valkey/valkey-bundle:9-alpine + container_name: valkey + ports: + - "6379:6379" + restart: unless-stopped + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + backend: + build: ./backend + container_name: ecommerce-backend + ports: + - "5000:5000" + environment: + - VALKEY_URL=redis://valkey:6379 + - PORT=5000 + - BREETH_API_KEY=ck_live_YhQWb-ZjIQxfs6AzDezC3zaPMPw4xJ8yRDkjIz7d16A + - FRONTEND_URL=http://localhost:3000 + depends_on: + valkey: + condition: service_healthy + restart: unless-stopped diff --git a/submissions/valkey-ecommerce-demo/documentation/css/bootstrap.css b/submissions/valkey-ecommerce-demo/documentation/css/bootstrap.css new file mode 100644 index 0000000..96a6b47 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/css/bootstrap.css @@ -0,0 +1,10 @@ +/*! + * Bootstrap v3.3.2 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + + *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + + + +html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px \9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.form-group-sm .form-control{height:30px;line-height:30px}select[multiple].form-group-sm .form-control,textarea.form-group-sm .form-control{height:auto}.form-group-sm .form-control-static{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.form-group-lg .form-control{height:46px;line-height:46px}select[multiple].form-group-lg .form-control,textarea.form-group-lg .form-control{height:auto}.form-group-lg .form-control-static{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.33px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.active,.btn-default.focus,.btn-default:active,.btn-default:focus,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.active,.btn-primary.focus,.btn-primary:active,.btn-primary:focus,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.active,.btn-success.focus,.btn-success:active,.btn-success:focus,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.active,.btn-info.focus,.btn-info:active,.btn-info:focus,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.active,.btn-warning.focus,.btn-warning:active,.btn-warning:focus,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.active,.btn-danger.focus,.btn-danger:active,.btn-danger:focus,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none;visibility:hidden}.collapse.in{display:block;visibility:visible}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none;visibility:hidden}.tab-content>.active{display:block;visibility:visible}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important;visibility:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:absolute;top:0;right:0;left:0;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;margin-top:-10px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} diff --git a/submissions/valkey-ecommerce-demo/documentation/css/font-awesome.css b/submissions/valkey-ecommerce-demo/documentation/css/font-awesome.css new file mode 100644 index 0000000..2dcdc22 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/css/font-awesome.css @@ -0,0 +1,1801 @@ +/*! + * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.3.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + transform: translate(0, 0); +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-genderless:before, +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} diff --git a/submissions/valkey-ecommerce-demo/documentation/css/scrollbar.css b/submissions/valkey-ecommerce-demo/documentation/css/scrollbar.css new file mode 100644 index 0000000..80eb263 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/css/scrollbar.css @@ -0,0 +1,1274 @@ +/* +== malihu jquery custom scrollbar plugin == +Plugin URI: http://manos.malihu.gr/jquery-custom-content-scroller +*/ + + + +/* +CONTENTS: + 1. BASIC STYLE - Plugin's basic/essential CSS properties (normally, should not be edited). + 2. VERTICAL SCROLLBAR - Positioning and dimensions of vertical scrollbar. + 3. HORIZONTAL SCROLLBAR - Positioning and dimensions of horizontal scrollbar. + 4. VERTICAL AND HORIZONTAL SCROLLBARS - Positioning and dimensions of 2-axis scrollbars. + 5. TRANSITIONS - CSS3 transitions for hover events, auto-expanded and auto-hidden scrollbars. + 6. SCROLLBAR COLORS, OPACITY AND BACKGROUNDS + 6.1 THEMES - Scrollbar colors, opacity, dimensions, backgrounds etc. via ready-to-use themes. +*/ + + + +/* +------------------------------------------------------------------------------------------------------------------------ +1. BASIC STYLE +------------------------------------------------------------------------------------------------------------------------ +*/ + + .mCustomScrollbar{ -ms-touch-action: none; touch-action: none; /* MSPointer events - direct all pointer events to js */ } + .mCustomScrollbar.mCS_no_scrollbar{ -ms-touch-action: auto; touch-action: auto; } + + .mCustomScrollBox{ /* contains plugin's markup */ + position: relative; + overflow: hidden; + height: 100%; + max-width: 100%; + outline: none; + direction: ltr; + } + + .mCSB_container{ /* contains the original content */ + overflow: hidden; + width: auto; + height: auto; + } + + + +/* +------------------------------------------------------------------------------------------------------------------------ +2. VERTICAL SCROLLBAR +y-axis +------------------------------------------------------------------------------------------------------------------------ +*/ + + .mCSB_inside > .mCSB_container{ margin-right: 30px; } + + .mCSB_container.mCS_no_scrollbar_y.mCS_y_hidden{ margin-right: 0; } /* non-visible scrollbar */ + + .mCS-dir-rtl > .mCSB_inside > .mCSB_container{ /* RTL direction/left-side scrollbar */ + margin-right: 0; + margin-left: 30px; + } + + .mCS-dir-rtl > .mCSB_inside > .mCSB_container.mCS_no_scrollbar_y.mCS_y_hidden{ margin-left: 0; } /* RTL direction/left-side scrollbar */ + + .mCSB_scrollTools{ /* contains scrollbar markup (draggable element, dragger rail, buttons etc.) */ + position: absolute; + width: 16px; + height: auto; + left: auto; + top: 0; + right: 0; + bottom: 0; + } + + .mCSB_outside + .mCSB_scrollTools{ right: -26px; } /* scrollbar position: outside */ + + .mCS-dir-rtl > .mCSB_inside > .mCSB_scrollTools, + .mCS-dir-rtl > .mCSB_outside + .mCSB_scrollTools{ /* RTL direction/left-side scrollbar */ + right: auto; + left: 0; + } + + .mCS-dir-rtl > .mCSB_outside + .mCSB_scrollTools{ left: -26px; } /* RTL direction/left-side scrollbar (scrollbar position: outside) */ + + .mCSB_scrollTools .mCSB_draggerContainer{ /* contains the draggable element and dragger rail markup */ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: auto; + } + + .mCSB_scrollTools a + .mCSB_draggerContainer{ margin: 20px 0; } + + .mCSB_scrollTools .mCSB_draggerRail{ + width: 7px; + height: 100%; + margin: 0 auto; + -webkit-border-radius: 0px; -moz-border-radius: 0px; border-radius: 0px; + background:#c1c1c1; + } + + .mCSB_scrollTools .mCSB_dragger{ /* the draggable element */ + cursor: pointer; + width: 100%; + height: 30px; /* minimum dragger height */ + z-index: 1; + } + + .mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ /* the dragger element */ + position: relative; + width: 7px; + height: 100%; + margin: 0 auto; + -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; + + text-align: center; + } + + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar{ width: 12px; /* auto-expanded scrollbar */ } + + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ width: 8px; /* auto-expanded scrollbar */ } + + .mCSB_scrollTools .mCSB_buttonUp, + .mCSB_scrollTools .mCSB_buttonDown{ + display: block; + position: absolute; + height: 20px; + width: 100%; + overflow: hidden; + margin: 0 auto; + cursor: pointer; + } + + .mCSB_scrollTools .mCSB_buttonDown{ bottom: 0; } + + + +/* +------------------------------------------------------------------------------------------------------------------------ +3. HORIZONTAL SCROLLBAR +x-axis +------------------------------------------------------------------------------------------------------------------------ +*/ + + .mCSB_horizontal.mCSB_inside > .mCSB_container{ + margin-right: 0; + margin-bottom: 30px; + } + + .mCSB_horizontal.mCSB_outside > .mCSB_container{ min-height: 100%; } + + .mCSB_horizontal > .mCSB_container.mCS_no_scrollbar_x.mCS_x_hidden{ margin-bottom: 0; } /* non-visible scrollbar */ + + .mCSB_scrollTools.mCSB_scrollTools_horizontal{ + width: auto; + height: 16px; + top: auto; + right: 0; + bottom: 0; + left: 0; + } + + .mCustomScrollBox + .mCSB_scrollTools.mCSB_scrollTools_horizontal, + .mCustomScrollBox + .mCSB_scrollTools + .mCSB_scrollTools.mCSB_scrollTools_horizontal{ bottom: -26px; } /* scrollbar position: outside */ + + .mCSB_scrollTools.mCSB_scrollTools_horizontal a + .mCSB_draggerContainer{ margin: 0 20px; } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + width: 100%; + height: 2px; + margin: 7px 0; + } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_dragger{ + width: 30px; /* minimum dragger width */ + height: 100%; + left: 0; + } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + width: 100%; + height: 4px; + margin: 6px auto; + } + + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar{ + height: 12px; /* auto-expanded scrollbar */ + margin: 2px auto; + } + + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ + height: 8px; /* auto-expanded scrollbar */ + margin: 4px 0; + } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_buttonLeft, + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_buttonRight{ + display: block; + position: absolute; + width: 20px; + height: 100%; + overflow: hidden; + margin: 0 auto; + cursor: pointer; + } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_buttonLeft{ left: 0; } + + .mCSB_scrollTools.mCSB_scrollTools_horizontal .mCSB_buttonRight{ right: 0; } + + + +/* +------------------------------------------------------------------------------------------------------------------------ +4. VERTICAL AND HORIZONTAL SCROLLBARS +yx-axis +------------------------------------------------------------------------------------------------------------------------ +*/ + + .mCSB_container_wrapper{ + position: absolute; + height: auto; + width: auto; + overflow: hidden; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin-right: 30px; + margin-bottom: 30px; + } + + .mCSB_container_wrapper > .mCSB_container{ + padding-right: 30px; + padding-bottom: 30px; + } + + .mCSB_vertical_horizontal > .mCSB_scrollTools.mCSB_scrollTools_vertical{ bottom: 20px; } + + .mCSB_vertical_horizontal > .mCSB_scrollTools.mCSB_scrollTools_horizontal{ right: 20px; } + + /* non-visible horizontal scrollbar */ + .mCSB_container_wrapper.mCS_no_scrollbar_x.mCS_x_hidden + .mCSB_scrollTools.mCSB_scrollTools_vertical{ bottom: 0; } + + /* non-visible vertical scrollbar/RTL direction/left-side scrollbar */ + .mCSB_container_wrapper.mCS_no_scrollbar_y.mCS_y_hidden + .mCSB_scrollTools ~ .mCSB_scrollTools.mCSB_scrollTools_horizontal, + .mCS-dir-rtl > .mCustomScrollBox.mCSB_vertical_horizontal.mCSB_inside > .mCSB_scrollTools.mCSB_scrollTools_horizontal{ right: 0; } + + /* RTL direction/left-side scrollbar */ + .mCS-dir-rtl > .mCustomScrollBox.mCSB_vertical_horizontal.mCSB_inside > .mCSB_scrollTools.mCSB_scrollTools_horizontal{ left: 20px; } + + /* non-visible scrollbar/RTL direction/left-side scrollbar */ + .mCS-dir-rtl > .mCustomScrollBox.mCSB_vertical_horizontal.mCSB_inside > .mCSB_container_wrapper.mCS_no_scrollbar_y.mCS_y_hidden + .mCSB_scrollTools ~ .mCSB_scrollTools.mCSB_scrollTools_horizontal{ left: 0; } + + .mCS-dir-rtl > .mCSB_inside > .mCSB_container_wrapper{ /* RTL direction/left-side scrollbar */ + margin-right: 0; + margin-left: 30px; + } + + .mCSB_container_wrapper.mCS_no_scrollbar_y.mCS_y_hidden > .mCSB_container{ + padding-right: 0; + -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; + } + + .mCSB_container_wrapper.mCS_no_scrollbar_x.mCS_x_hidden > .mCSB_container{ + padding-bottom: 0; + -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; + } + + .mCustomScrollBox.mCSB_vertical_horizontal.mCSB_inside > .mCSB_container_wrapper.mCS_no_scrollbar_y.mCS_y_hidden{ + margin-right: 0; /* non-visible scrollbar */ + margin-left: 0; + } + + /* non-visible horizontal scrollbar */ + .mCustomScrollBox.mCSB_vertical_horizontal.mCSB_inside > .mCSB_container_wrapper.mCS_no_scrollbar_x.mCS_x_hidden{ margin-bottom: 0; } + + + +/* +------------------------------------------------------------------------------------------------------------------------ +5. TRANSITIONS +------------------------------------------------------------------------------------------------------------------------ +*/ + + .mCSB_scrollTools, + .mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCSB_scrollTools .mCSB_buttonUp, + .mCSB_scrollTools .mCSB_buttonDown, + .mCSB_scrollTools .mCSB_buttonLeft, + .mCSB_scrollTools .mCSB_buttonRight{ + -webkit-transition: opacity .2s ease-in-out, background-color .2s ease-in-out; + -moz-transition: opacity .2s ease-in-out, background-color .2s ease-in-out; + -o-transition: opacity .2s ease-in-out, background-color .2s ease-in-out; + transition: opacity .2s ease-in-out, background-color .2s ease-in-out; + } + + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger_bar, /* auto-expanded scrollbar */ + .mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerRail, + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger_bar, + .mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerRail{ + -webkit-transition: width .2s ease-out .2s, height .2s ease-out .2s, + margin-left .2s ease-out .2s, margin-right .2s ease-out .2s, + margin-top .2s ease-out .2s, margin-bottom .2s ease-out .2s, + opacity .2s ease-in-out, background-color .2s ease-in-out; + -moz-transition: width .2s ease-out .2s, height .2s ease-out .2s, + margin-left .2s ease-out .2s, margin-right .2s ease-out .2s, + margin-top .2s ease-out .2s, margin-bottom .2s ease-out .2s, + opacity .2s ease-in-out, background-color .2s ease-in-out; + -o-transition: width .2s ease-out .2s, height .2s ease-out .2s, + margin-left .2s ease-out .2s, margin-right .2s ease-out .2s, + margin-top .2s ease-out .2s, margin-bottom .2s ease-out .2s, + opacity .2s ease-in-out, background-color .2s ease-in-out; + transition: width .2s ease-out .2s, height .2s ease-out .2s, + margin-left .2s ease-out .2s, margin-right .2s ease-out .2s, + margin-top .2s ease-out .2s, margin-bottom .2s ease-out .2s, + opacity .2s ease-in-out, background-color .2s ease-in-out; + } + + + +/* +------------------------------------------------------------------------------------------------------------------------ +6. SCROLLBAR COLORS, OPACITY AND BACKGROUNDS +------------------------------------------------------------------------------------------------------------------------ +*/ + + /* + ---------------------------------------- + 6.1 THEMES + ---------------------------------------- + */ + + /* default theme ("light") */ + + .mCSB_scrollTools{ opacity: 0.75; filter: "alpha(opacity=75)"; -ms-filter: "alpha(opacity=75)"; } + + .mCS-autoHide > .mCustomScrollBox > .mCSB_scrollTools, + .mCS-autoHide > .mCustomScrollBox ~ .mCSB_scrollTools{ opacity: 0; filter: "alpha(opacity=0)"; -ms-filter: "alpha(opacity=0)"; } + + .mCustomScrollbar > .mCustomScrollBox > .mCSB_scrollTools.mCSB_scrollTools_onDrag, + .mCustomScrollbar > .mCustomScrollBox ~ .mCSB_scrollTools.mCSB_scrollTools_onDrag, + .mCustomScrollBox:hover > .mCSB_scrollTools, + .mCustomScrollBox:hover ~ .mCSB_scrollTools, + .mCS-autoHide:hover > .mCustomScrollBox > .mCSB_scrollTools, + .mCS-autoHide:hover > .mCustomScrollBox ~ .mCSB_scrollTools{ opacity: 1; filter: "alpha(opacity=100)"; -ms-filter: "alpha(opacity=100)"; } + + .mCSB_scrollTools .mCSB_draggerRail{ + background-color: #000; background-color: rgba(0,0,0,0); + filter: "alpha(opacity=0)"; -ms-filter: "alpha(opacity=0)"; + } + + .mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-color: #fff; background-color: rgba(255,255,255,0.75); + filter: "alpha(opacity=75)"; -ms-filter: "alpha(opacity=75)"; + } + + .mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ + background-color: #fff; background-color: rgba(255,255,255,0.85); + filter: "alpha(opacity=85)"; -ms-filter: "alpha(opacity=85)"; + } + .mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ + background-color: #fff; background-color: rgba(255,255,255,0.9); + filter: "alpha(opacity=90)"; -ms-filter: "alpha(opacity=90)"; + } + + .mCSB_scrollTools .mCSB_buttonUp, + .mCSB_scrollTools .mCSB_buttonDown, + .mCSB_scrollTools .mCSB_buttonLeft, + .mCSB_scrollTools .mCSB_buttonRight{ + background-image: url(mCSB_buttons.png); /* css sprites */ + background-repeat: no-repeat; + opacity: 0.4; filter: "alpha(opacity=40)"; -ms-filter: "alpha(opacity=40)"; + } + + .mCSB_scrollTools .mCSB_buttonUp{ + background-position: 0 0; + /* + sprites locations + light: 0 0, -16px 0, -32px 0, -48px 0, 0 -72px, -16px -72px, -32px -72px + dark: -80px 0, -96px 0, -112px 0, -128px 0, -80px -72px, -96px -72px, -112px -72px + */ + } + + .mCSB_scrollTools .mCSB_buttonDown{ + background-position: 0 -20px; + /* + sprites locations + light: 0 -20px, -16px -20px, -32px -20px, -48px -20px, 0 -92px, -16px -92px, -32px -92px + dark: -80px -20px, -96px -20px, -112px -20px, -128px -20px, -80px -92px, -96px -92px, -112 -92px + */ + } + + .mCSB_scrollTools .mCSB_buttonLeft{ + background-position: 0 -40px; + /* + sprites locations + light: 0 -40px, -20px -40px, -40px -40px, -60px -40px, 0 -112px, -20px -112px, -40px -112px + dark: -80px -40px, -100px -40px, -120px -40px, -140px -40px, -80px -112px, -100px -112px, -120px -112px + */ + } + + .mCSB_scrollTools .mCSB_buttonRight{ + background-position: 0 -56px; + /* + sprites locations + light: 0 -56px, -20px -56px, -40px -56px, -60px -56px, 0 -128px, -20px -128px, -40px -128px + dark: -80px -56px, -100px -56px, -120px -56px, -140px -56px, -80px -128px, -100px -128px, -120px -128px + */ + } + + .mCSB_scrollTools .mCSB_buttonUp:hover, + .mCSB_scrollTools .mCSB_buttonDown:hover, + .mCSB_scrollTools .mCSB_buttonLeft:hover, + .mCSB_scrollTools .mCSB_buttonRight:hover{ opacity: 0.75; filter: "alpha(opacity=75)"; -ms-filter: "alpha(opacity=75)"; } + + .mCSB_scrollTools .mCSB_buttonUp:active, + .mCSB_scrollTools .mCSB_buttonDown:active, + .mCSB_scrollTools .mCSB_buttonLeft:active, + .mCSB_scrollTools .mCSB_buttonRight:active{ opacity: 0.9; filter: "alpha(opacity=90)"; -ms-filter: "alpha(opacity=90)"; } + + + /* theme: "dark" */ + + .mCS-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.15); } + + .mCS-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: rgba(0,0,0,0.85); } + + .mCS-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: rgba(0,0,0,0.9); } + + .mCS-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -80px 0; } + + .mCS-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -80px -20px; } + + .mCS-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -80px -40px; } + + .mCS-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -80px -56px; } + + /* ---------------------------------------- */ + + + + /* theme: "light-2", "dark-2" */ + + .mCS-light-2.mCSB_scrollTools .mCSB_draggerRail, + .mCS-dark-2.mCSB_scrollTools .mCSB_draggerRail{ + width: 4px; + background-color: #fff; background-color: rgba(255,255,255,0.1); + -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; + } + + .mCS-light-2.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-2.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + width: 4px; + background-color: #fff; background-color: rgba(255,255,255,0.75); + -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; + } + + .mCS-light-2.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-dark-2.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-light-2.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-2.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + width: 100%; + height: 4px; + margin: 6px auto; + } + + .mCS-light-2.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.85); } + + .mCS-light-2.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-light-2.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.9); } + + .mCS-light-2.mCSB_scrollTools .mCSB_buttonUp{ background-position: -32px 0; } + + .mCS-light-2.mCSB_scrollTools .mCSB_buttonDown{ background-position: -32px -20px; } + + .mCS-light-2.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -40px -40px; } + + .mCS-light-2.mCSB_scrollTools .mCSB_buttonRight{ background-position: -40px -56px; } + + + /* theme: "dark-2" */ + + .mCS-dark-2.mCSB_scrollTools .mCSB_draggerRail{ + background-color: #000; background-color: rgba(0,0,0,0.1); + -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; + } + + .mCS-dark-2.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-color: #000; background-color: rgba(0,0,0,0.75); + -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; + } + + .mCS-dark-2.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-dark-2.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-dark-2.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-dark-2.mCSB_scrollTools .mCSB_buttonUp{ background-position: -112px 0; } + + .mCS-dark-2.mCSB_scrollTools .mCSB_buttonDown{ background-position: -112px -20px; } + + .mCS-dark-2.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -120px -40px; } + + .mCS-dark-2.mCSB_scrollTools .mCSB_buttonRight{ background-position: -120px -56px; } + + /* ---------------------------------------- */ + + + + /* theme: "light-thick", "dark-thick" */ + + .mCS-light-thick.mCSB_scrollTools .mCSB_draggerRail, + .mCS-dark-thick.mCSB_scrollTools .mCSB_draggerRail{ + width: 4px; + background-color: #fff; background-color: rgba(255,255,255,0.1); + -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; + } + + .mCS-light-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + width: 6px; + background-color: #fff; background-color: rgba(255,255,255,0.75); + -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; + } + + .mCS-light-thick.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-dark-thick.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + width: 100%; + height: 4px; + margin: 6px 0; + } + + .mCS-light-thick.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-thick.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + width: 100%; + height: 6px; + margin: 5px auto; + } + + .mCS-light-thick.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.85); } + + .mCS-light-thick.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-light-thick.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.9); } + + .mCS-light-thick.mCSB_scrollTools .mCSB_buttonUp{ background-position: -16px 0; } + + .mCS-light-thick.mCSB_scrollTools .mCSB_buttonDown{ background-position: -16px -20px; } + + .mCS-light-thick.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -20px -40px; } + + .mCS-light-thick.mCSB_scrollTools .mCSB_buttonRight{ background-position: -20px -56px; } + + + /* theme: "dark-thick" */ + + .mCS-dark-thick.mCSB_scrollTools .mCSB_draggerRail{ + background-color: #000; background-color: rgba(0,0,0,0.1); + -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; + } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-color: #000; background-color: rgba(0,0,0,0.75); + -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; + } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-dark-thick.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_buttonUp{ background-position: -96px 0; } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_buttonDown{ background-position: -96px -20px; } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -100px -40px; } + + .mCS-dark-thick.mCSB_scrollTools .mCSB_buttonRight{ background-position: -100px -56px; } + + /* ---------------------------------------- */ + + + + /* theme: "light-thin", "dark-thin" */ + + .mCS-light-thin.mCSB_scrollTools .mCSB_draggerRail{ background-color: #fff; background-color: rgba(255,255,255,0.1); } + + .mCS-light-thin.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-thin.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ width: 2px; } + + .mCS-light-thin.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-dark-thin.mCSB_scrollTools_horizontal .mCSB_draggerRail{ width: 100%; } + + .mCS-light-thin.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-thin.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + width: 100%; + height: 2px; + margin: 7px auto; + } + + + /* theme "dark-thin" */ + + .mCS-dark-thin.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.15); } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-dark-thin.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_buttonUp{ background-position: -80px 0; } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_buttonDown{ background-position: -80px -20px; } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -80px -40px; } + + .mCS-dark-thin.mCSB_scrollTools .mCSB_buttonRight{ background-position: -80px -56px; } + + /* ---------------------------------------- */ + + + + /* theme "rounded", "rounded-dark", "rounded-dots", "rounded-dots-dark" */ + + .mCS-rounded.mCSB_scrollTools .mCSB_draggerRail{ background-color: #fff; background-color: rgba(255,255,255,0.15); } + + .mCS-rounded.mCSB_scrollTools .mCSB_dragger, + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger, + .mCS-rounded-dots.mCSB_scrollTools .mCSB_dragger, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger{ height: 14px; } + + .mCS-rounded.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dots.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + width: 14px; + margin: 0 1px; + } + + .mCS-rounded.mCSB_scrollTools_horizontal .mCSB_dragger, + .mCS-rounded-dark.mCSB_scrollTools_horizontal .mCSB_dragger, + .mCS-rounded-dots.mCSB_scrollTools_horizontal .mCSB_dragger, + .mCS-rounded-dots-dark.mCSB_scrollTools_horizontal .mCSB_dragger{ width: 14px; } + + .mCS-rounded.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dots.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + height: 14px; + margin: 1px 0; + } + + .mCS-rounded.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCS-rounded.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar{ + width: 16px; /* auto-expanded scrollbar */ + height: 16px; + margin: -1px 0; + } + + .mCS-rounded.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-rounded.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail, + .mCS-rounded-dark.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-rounded-dark.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ width: 4px; /* auto-expanded scrollbar */ } + + .mCS-rounded.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCS-rounded.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_dragger .mCSB_dragger_bar{ + height: 16px; /* auto-expanded scrollbar */ + width: 16px; + margin: 0 -1px; + } + + .mCS-rounded.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-rounded.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail, + .mCS-rounded-dark.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-rounded-dark.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ + height: 4px; /* auto-expanded scrollbar */ + margin: 6px 0; + } + + .mCS-rounded.mCSB_scrollTools .mCSB_buttonUp{ background-position: 0 -72px; } + + .mCS-rounded.mCSB_scrollTools .mCSB_buttonDown{ background-position: 0 -92px; } + + .mCS-rounded.mCSB_scrollTools .mCSB_buttonLeft{ background-position: 0 -112px; } + + .mCS-rounded.mCSB_scrollTools .mCSB_buttonRight{ background-position: 0 -128px; } + + + /* theme "rounded-dark", "rounded-dots-dark" */ + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.15); } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-rounded-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -80px -72px; } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -80px -92px; } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -80px -112px; } + + .mCS-rounded-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -80px -128px; } + + + /* theme "rounded-dots", "rounded-dots-dark" */ + + .mCS-rounded-dots.mCSB_scrollTools_vertical .mCSB_draggerRail, + .mCS-rounded-dots-dark.mCSB_scrollTools_vertical .mCSB_draggerRail{ width: 4px; } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_draggerRail, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-rounded-dots.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-rounded-dots-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + background-color: transparent; + background-position: center; + } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_draggerRail, + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_draggerRail{ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAANElEQVQYV2NkIAAYiVbw//9/Y6DiM1ANJoyMjGdBbLgJQAX/kU0DKgDLkaQAvxW4HEvQFwCRcxIJK1XznAAAAABJRU5ErkJggg=="); + background-repeat: repeat-y; + opacity: 0.3; + filter: "alpha(opacity=30)"; -ms-filter: "alpha(opacity=30)"; + } + + .mCS-rounded-dots.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-rounded-dots-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + height: 4px; + margin: 6px 0; + background-repeat: repeat-x; + } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_buttonUp{ background-position: -16px -72px; } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_buttonDown{ background-position: -16px -92px; } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -20px -112px; } + + .mCS-rounded-dots.mCSB_scrollTools .mCSB_buttonRight{ background-position: -20px -128px; } + + + /* theme "rounded-dots-dark" */ + + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_draggerRail{ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAALElEQVQYV2NkIAAYSVFgDFR8BqrBBEifBbGRTfiPZhpYjiQFBK3A6l6CvgAAE9kGCd1mvgEAAAAASUVORK5CYII="); + } + + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -96px -72px; } + + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -96px -92px; } + + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -100px -112px; } + + .mCS-rounded-dots-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -100px -128px; } + + /* ---------------------------------------- */ + + + + /* theme "3d", "3d-dark", "3d-thick", "3d-thick-dark" */ + + .mCS-3d.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-repeat: repeat-y; + background-image: -moz-linear-gradient(left, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 100%); + background-image: -webkit-gradient(linear, left top, right top, color-stop(0%,rgba(255,255,255,0.5)), color-stop(100%,rgba(255,255,255,0))); + background-image: -webkit-linear-gradient(left, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: -o-linear-gradient(left, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: -ms-linear-gradient(left, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: linear-gradient(to right, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + } + + .mCS-3d.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + background-repeat: repeat-x; + background-image: -moz-linear-gradient(top, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 100%); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.5)), color-stop(100%,rgba(255,255,255,0))); + background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: -o-linear-gradient(top, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: -ms-linear-gradient(top, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + background-image: linear-gradient(to bottom, rgba(255,255,255,0.5) 0%,rgba(255,255,255,0) 100%); + } + + + /* theme "3d", "3d-dark" */ + + .mCS-3d.mCSB_scrollTools_vertical .mCSB_dragger, + .mCS-3d-dark.mCSB_scrollTools_vertical .mCSB_dragger{ height: 70px; } + + .mCS-3d.mCSB_scrollTools_horizontal .mCSB_dragger, + .mCS-3d-dark.mCSB_scrollTools_horizontal .mCSB_dragger{ width: 70px; } + + .mCS-3d.mCSB_scrollTools, + .mCS-3d-dark.mCSB_scrollTools{ + opacity: 1; + filter: "alpha(opacity=30)"; -ms-filter: "alpha(opacity=30)"; + } + + .mCS-3d.mCSB_scrollTools .mCSB_draggerRail, + .mCS-3d.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ -webkit-border-radius: 16px; -moz-border-radius: 16px; border-radius: 16px; } + + .mCS-3d.mCSB_scrollTools .mCSB_draggerRail, + .mCS-3d-dark.mCSB_scrollTools .mCSB_draggerRail{ + width: 8px; + background-color: #000; background-color: rgba(0,0,0,0.2); + box-shadow: inset 1px 0 1px rgba(0,0,0,0.5), inset -1px 0 1px rgba(255,255,255,0.2); + } + + .mCS-3d.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-3d.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-3d.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #555; } + + .mCS-3d.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ width: 8px; } + + .mCS-3d.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-3d-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + width: 100%; + height: 8px; + margin: 4px 0; + box-shadow: inset 0 1px 1px rgba(0,0,0,0.5), inset 0 -1px 1px rgba(255,255,255,0.2); + } + + .mCS-3d.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + width: 100%; + height: 8px; + margin: 4px auto; + } + + .mCS-3d.mCSB_scrollTools .mCSB_buttonUp{ background-position: -32px -72px; } + + .mCS-3d.mCSB_scrollTools .mCSB_buttonDown{ background-position: -32px -92px; } + + .mCS-3d.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -40px -112px; } + + .mCS-3d.mCSB_scrollTools .mCSB_buttonRight{ background-position: -40px -128px; } + + + /* theme "3d-dark" */ + + .mCS-3d-dark.mCSB_scrollTools .mCSB_draggerRail{ + background-color: #000; background-color: rgba(0,0,0,0.1); + box-shadow: inset 1px 0 1px rgba(0,0,0,0.1); + } + + .mCS-3d-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail{ box-shadow: inset 0 1px 1px rgba(0,0,0,0.1); } + + .mCS-3d-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -112px -72px; } + + .mCS-3d-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -112px -92px; } + + .mCS-3d-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -120px -112px; } + + .mCS-3d-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -120px -128px; } + + /* ---------------------------------------- */ + + + + /* theme: "3d-thick", "3d-thick-dark" */ + + .mCS-3d-thick.mCSB_scrollTools, + .mCS-3d-thick-dark.mCSB_scrollTools{ + opacity: 1; + filter: "alpha(opacity=30)"; -ms-filter: "alpha(opacity=30)"; + } + + .mCS-3d-thick.mCSB_scrollTools, + .mCS-3d-thick-dark.mCSB_scrollTools, + .mCS-3d-thick.mCSB_scrollTools .mCSB_draggerContainer, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_draggerContainer{ -webkit-border-radius: 7px; -moz-border-radius: 7px; border-radius: 7px; } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } + + .mCSB_inside + .mCS-3d-thick.mCSB_scrollTools_vertical, + .mCSB_inside + .mCS-3d-thick-dark.mCSB_scrollTools_vertical{ right: 1px; } + + .mCS-3d-thick.mCSB_scrollTools_vertical, + .mCS-3d-thick-dark.mCSB_scrollTools_vertical{ box-shadow: inset 1px 0 1px rgba(0,0,0,0.1), inset 0 0 14px rgba(0,0,0,0.5); } + + .mCS-3d-thick.mCSB_scrollTools_horizontal, + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal{ + bottom: 1px; + box-shadow: inset 0 1px 1px rgba(0,0,0,0.1), inset 0 0 14px rgba(0,0,0,0.5); + } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + box-shadow: inset 1px 0 0 rgba(255,255,255,0.4); + width: 12px; + margin: 2px; + position: absolute; + height: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .mCS-3d-thick.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ box-shadow: inset 0 1px 0 rgba(255,255,255,0.4); } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-3d-thick.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #555; } + + .mCS-3d-thick.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + height: 12px; + width: auto; + } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_draggerContainer{ + background-color: #000; background-color: rgba(0,0,0,0.05); + box-shadow: inset 1px 1px 16px rgba(0,0,0,0.1); + } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_draggerRail{ background-color: transparent; } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_buttonUp{ background-position: -32px -72px; } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_buttonDown{ background-position: -32px -92px; } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -40px -112px; } + + .mCS-3d-thick.mCSB_scrollTools .mCSB_buttonRight{ background-position: -40px -128px; } + + + /* theme: "3d-thick-dark" */ + + .mCS-3d-thick-dark.mCSB_scrollTools{ box-shadow: inset 0 0 14px rgba(0,0,0,0.2); } + + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal{ box-shadow: inset 0 1px 1px rgba(0,0,0,0.1), inset 0 0 14px rgba(0,0,0,0.2); } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ box-shadow: inset 1px 0 0 rgba(255,255,255,0.4), inset -1px 0 0 rgba(0,0,0,0.2); } + + .mCS-3d-thick-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), inset 0 -1px 0 rgba(0,0,0,0.2); } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #777; } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_draggerContainer{ + background-color: #fff; background-color: rgba(0,0,0,0.05); + box-shadow: inset 1px 1px 16px rgba(0,0,0,0.1); + } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: transparent; } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -112px -72px; } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -112px -92px; } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -120px -112px; } + + .mCS-3d-thick-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -120px -128px; } + + /* ---------------------------------------- */ + + + + /* theme: "minimal", "minimal-dark" */ + + .mCSB_outside + .mCS-minimal.mCSB_scrollTools_vertical, + .mCSB_outside + .mCS-minimal-dark.mCSB_scrollTools_vertical{ + right: 0; + margin: 12px 0; + } + + .mCustomScrollBox.mCS-minimal + .mCSB_scrollTools.mCSB_scrollTools_horizontal, + .mCustomScrollBox.mCS-minimal + .mCSB_scrollTools + .mCSB_scrollTools.mCSB_scrollTools_horizontal, + .mCustomScrollBox.mCS-minimal-dark + .mCSB_scrollTools.mCSB_scrollTools_horizontal, + .mCustomScrollBox.mCS-minimal-dark + .mCSB_scrollTools + .mCSB_scrollTools.mCSB_scrollTools_horizontal{ + bottom: 0; + margin: 0 12px; + } + + /* RTL direction/left-side scrollbar */ + .mCS-dir-rtl > .mCSB_outside + .mCS-minimal.mCSB_scrollTools_vertical, + .mCS-dir-rtl > .mCSB_outside + .mCS-minimal-dark.mCSB_scrollTools_vertical{ + left: 0; + right: auto; + } + + .mCS-minimal.mCSB_scrollTools .mCSB_draggerRail, + .mCS-minimal-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: transparent; } + + .mCS-minimal.mCSB_scrollTools_vertical .mCSB_dragger, + .mCS-minimal-dark.mCSB_scrollTools_vertical .mCSB_dragger{ height: 50px; } + + .mCS-minimal.mCSB_scrollTools_horizontal .mCSB_dragger, + .mCS-minimal-dark.mCSB_scrollTools_horizontal .mCSB_dragger{ width: 50px; } + + .mCS-minimal.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-color: #fff; background-color: rgba(255,255,255,0.2); + filter: "alpha(opacity=20)"; -ms-filter: "alpha(opacity=20)"; + } + + .mCS-minimal.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-minimal.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ + background-color: #fff; background-color: rgba(255,255,255,0.5); + filter: "alpha(opacity=50)"; -ms-filter: "alpha(opacity=50)"; + } + + + /* theme: "minimal-dark" */ + + .mCS-minimal-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + background-color: #000; background-color: rgba(0,0,0,0.2); + filter: "alpha(opacity=20)"; -ms-filter: "alpha(opacity=20)"; + } + + .mCS-minimal-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-minimal-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ + background-color: #000; background-color: rgba(0,0,0,0.5); + filter: "alpha(opacity=50)"; -ms-filter: "alpha(opacity=50)"; + } + + /* ---------------------------------------- */ + + + + /* theme "light-3", "dark-3" */ + + .mCS-light-3.mCSB_scrollTools .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools .mCSB_draggerRail{ + width: 6px; + background-color: #000; background-color: rgba(0,0,0,0.2); + } + + .mCS-light-3.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-3.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ width: 6px; } + + .mCS-light-3.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-dark-3.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-light-3.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + width: 100%; + height: 6px; + margin: 5px 0; + } + + .mCS-light-3.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-light-3.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools_vertical.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ + width: 12px; + } + + .mCS-light-3.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-light-3.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_dragger.mCSB_dragger_onDrag_expanded + .mCSB_draggerRail, + .mCS-dark-3.mCSB_scrollTools_horizontal.mCSB_scrollTools_onDrag_expand .mCSB_draggerContainer:hover .mCSB_draggerRail{ + height: 12px; + margin: 2px 0; + } + + .mCS-light-3.mCSB_scrollTools .mCSB_buttonUp{ background-position: -32px -72px; } + + .mCS-light-3.mCSB_scrollTools .mCSB_buttonDown{ background-position: -32px -92px; } + + .mCS-light-3.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -40px -112px; } + + .mCS-light-3.mCSB_scrollTools .mCSB_buttonRight{ background-position: -40px -128px; } + + + /* theme "dark-3" */ + + .mCS-dark-3.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-dark-3.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-dark-3.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-dark-3.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-dark-3.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.1); } + + .mCS-dark-3.mCSB_scrollTools .mCSB_buttonUp{ background-position: -112px -72px; } + + .mCS-dark-3.mCSB_scrollTools .mCSB_buttonDown{ background-position: -112px -92px; } + + .mCS-dark-3.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -120px -112px; } + + .mCS-dark-3.mCSB_scrollTools .mCSB_buttonRight{ background-position: -120px -128px; } + + /* ---------------------------------------- */ + + + + /* theme "inset", "inset-dark", "inset-2", "inset-2-dark", "inset-3", "inset-3-dark" */ + + .mCS-inset.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-2.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-3.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_draggerRail{ + width: 12px; + background-color: #000; background-color: rgba(0,0,0,0.2); + } + + .mCS-inset.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-2.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-3.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ + width: 6px; + margin: 3px 5px; + position: absolute; + height: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .mCS-inset.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-2.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-3.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools_horizontal .mCSB_dragger .mCSB_dragger_bar{ + height: 6px; + margin: 5px 3px; + position: absolute; + width: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .mCS-inset.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-inset-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-inset-2.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-inset-2-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-inset-3.mCSB_scrollTools_horizontal .mCSB_draggerRail, + .mCS-inset-3-dark.mCSB_scrollTools_horizontal .mCSB_draggerRail{ + width: 100%; + height: 12px; + margin: 2px 0; + } + + .mCS-inset.mCSB_scrollTools .mCSB_buttonUp, + .mCS-inset-2.mCSB_scrollTools .mCSB_buttonUp, + .mCS-inset-3.mCSB_scrollTools .mCSB_buttonUp{ background-position: -32px -72px; } + + .mCS-inset.mCSB_scrollTools .mCSB_buttonDown, + .mCS-inset-2.mCSB_scrollTools .mCSB_buttonDown, + .mCS-inset-3.mCSB_scrollTools .mCSB_buttonDown{ background-position: -32px -92px; } + + .mCS-inset.mCSB_scrollTools .mCSB_buttonLeft, + .mCS-inset-2.mCSB_scrollTools .mCSB_buttonLeft, + .mCS-inset-3.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -40px -112px; } + + .mCS-inset.mCSB_scrollTools .mCSB_buttonRight, + .mCS-inset-2.mCSB_scrollTools .mCSB_buttonRight, + .mCS-inset-3.mCSB_scrollTools .mCSB_buttonRight{ background-position: -40px -128px; } + + + /* theme "inset-dark", "inset-2-dark", "inset-3-dark" */ + + .mCS-inset-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-inset-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.1); } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_buttonUp, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_buttonUp, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_buttonUp{ background-position: -112px -72px; } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_buttonDown, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_buttonDown, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_buttonDown{ background-position: -112px -92px; } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_buttonLeft, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_buttonLeft, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_buttonLeft{ background-position: -120px -112px; } + + .mCS-inset-dark.mCSB_scrollTools .mCSB_buttonRight, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_buttonRight, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_buttonRight{ background-position: -120px -128px; } + + + /* theme "inset-2", "inset-2-dark" */ + + .mCS-inset-2.mCSB_scrollTools .mCSB_draggerRail, + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_draggerRail{ + background-color: transparent; + border-width: 1px; + border-style: solid; + border-color: #fff; + border-color: rgba(255,255,255,0.2); + -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; + } + + .mCS-inset-2-dark.mCSB_scrollTools .mCSB_draggerRail{ border-color: #000; border-color: rgba(0,0,0,0.2); } + + + /* theme "inset-3", "inset-3-dark" */ + + .mCS-inset-3.mCSB_scrollTools .mCSB_draggerRail{ background-color: #fff; background-color: rgba(255,255,255,0.6); } + + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_draggerRail{ background-color: #000; background-color: rgba(0,0,0,0.6); } + + .mCS-inset-3.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.75); } + + .mCS-inset-3.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.85); } + + .mCS-inset-3.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-inset-3.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #000; background-color: rgba(0,0,0,0.9); } + + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.75); } + + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger:hover .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.85); } + + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger:active .mCSB_dragger_bar, + .mCS-inset-3-dark.mCSB_scrollTools .mCSB_dragger.mCSB_dragger_onDrag .mCSB_dragger_bar{ background-color: #fff; background-color: rgba(255,255,255,0.9); } + + /* ---------------------------------------- */ diff --git a/submissions/valkey-ecommerce-demo/documentation/css/style.css b/submissions/valkey-ecommerce-demo/documentation/css/style.css new file mode 100644 index 0000000..aa5709d --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/css/style.css @@ -0,0 +1,384 @@ +/* Documentation For Quray HTML5 Agency Template */ + +/*** + +==================================================================== + Fonts +==================================================================== + +***/ + +@import url(http://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700); +@import url(http://fonts.googleapis.com/css?family=Open+Sans+Condensed:300,700); + +@import url("font-awesome.css"); + +/*** + +==================================================================== + Reset +==================================================================== + +***/ + +/*** + +==================================================================== + Global Settings +==================================================================== + + ***/ + +body { + font-family: "Open Sans", sans-serif; + font-size: 14px; + color: #353535; + line-height: 1.6em; + font-weight: 500; + background: #ffffff; + -webkit-font-smoothing: antialiased; +} + +a { + text-decoration: none; + cursor: pointer; + color: #246BFD; +} +ul, +ol { + list-style-type: none; + padding: 0; +} +a:hover, +a:focus, +a:visited { + text-decoration: none; + outline: none; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + position: relative; + margin: 0px; + background: none; +} + +h1 { + font-size: 24px; +} + +h2 { + font-size: 20px; +} + +h3 { + font-size: 18px; +} + +p { + font-size: 14px; + line-height: 1.6em; +} + +/*** + +==================================================================== + Sidebar +==================================================================== + +****/ +#sidebar { + position: fixed; + left: 0px; + top: 0px; + width: 250px; + height: 100%; + padding: 0px 20px; + border-right: 2px solid #246BFD; + z-index: 10; +} + +#sidebar .mCSB_inside > .mCSB_container { + margin-right: 12px !important; +} + +#sidebar .mCS_no_scrollbar .mCSB_container { + margin-right: 0px !important; +} + +#sidebar .menu-box { + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + overflow: auto; + padding: 0px 0px 30px; + background: rgba(0, 0, 0, 0.8); + transition: all 1000ms ease; + -moz-transition: all 1000ms ease; + -webkit-transition: all 1000ms ease; + -ms-transition: all 1000ms ease; + -o-transition: all 1000ms ease; +} + +#sidebar .menu-box .logo { + position: relative; + margin-bottom: 35px; + padding: 30px 32px 10px; +} + +#sidebar .menu-box .logo img { + position: relative; + display: inline-block; + max-width: 100%; +} + +#sidebar .menu-box .sticky-menu { + position: relative; +} + +#sidebar .menu-box .sticky-menu > ul { + position: relative; + display: block; + margin: 0px; + padding: 0px; + border-top: 1px solid rgba(255, 255, 255, 0.2); +} + +#sidebar .menu-box .sticky-menu > ul > li { + position: relative; + display: block; + padding: 3px 30px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + font-size: 20px; + color: #ffffff; + text-align: left; +} + +#sidebar .menu-box .sticky-menu > ul > li > a { + position: relative; + display: block; + padding: 7px 30px 7px 0px; + color: #ffffff; + font-size: 13px; + font-weight: 600; + line-height: 20px; + text-transform: uppercase; + transition: all 300ms ease; + -moz-transition: all 300ms ease; + -webkit-transition: all 300ms ease; + -ms-transition: all 300ms ease; + -o-transition: all 300ms ease; +} + +#sidebar .menu-box .sticky-menu > ul > li a .fa { + position: absolute; + right: -10px; + top: 0px; + width: 24px; + text-align: left; + color: #ffffff; + opacity: 0; + font-size: 12px; + display: block; + line-height: 34px; + transition: all 500ms ease; + -moz-transition: all 500ms ease; + -webkit-transition: all 500ms ease; + -ms-transition: all 500ms ease; + -o-transition: all 500ms ease; +} + +#sidebar .menu-box .sticky-menu > ul > li.current a .fa, +#sidebar .menu-box .sticky-menu > ul > li:hover a .fa { + /* left:15px; */ + opacity: 1; +} + +#sidebar .menu-box .sticky-menu > ul > li > a:hover, +#sidebar .menu-box .sticky-menu > ul > li.current > a, +#sidebar .menu-box .sticky-menu > ul > li.current-menu-item > a { + text-shadow: 0px 0px 0px rgba(255, 255, 255, 0.7), + 0px 8px 13px rgba(255, 255, 255, 0.1), + 0px 18px 23px rgba(255, 255, 255, 0.1); +} + +#sidebar .copyright { + position: fixed; + left: 0px; + bottom: 5px; + width: 250px; + height: 30px; + font-size: 13px; + color: #ffffff; + line-height: 30px; + text-align: center; +} + +/*** + +==================================================================== + Content Section +==================================================================== + +****/ + +#content-section { + position: relative; + padding: 0px 0px 30px 250px; + width: 100%; + z-index: 5; +} + +#content-section .section { + position: relative; + min-height: 200px; + margin-bottom: 50px; +} + +#content-section .sec-title { + position: relative; + padding: 10px 20px 10px 50px; + background: rgba(0, 0, 0, 0.7); + color: #ffffff; +} + +#content-section .sec-title h2 { + position: relative; + font-size: 24px; + text-transform: capitalize; + font-weight: 600; +} + +#content-section .sec-title h3 { + position: relative; + font-size: 22px; + text-transform: uppercase; + font-weight: 600; +} + +#content-section .sec-title .icon { + position: absolute; + left: 20px; + top: 10px; + width: 30px; + height: 40px; + line-height: 30px; + font-size: 18px; +} + +#content-section .separator { + position: relative; + margin: 20px 0px; + height: 1px; +} + +#content-section .separator:before { + content: ""; + position: relative; + display: block; + width: 100%; + height: 1px; + background: linear-gradient(left, #d0d0d0, #f1f1f1, #ffffff); + background: -moz-linear-gradient(left, #d0d0d0, #f1f1f1, #ffffff); + background: -webkit-linear-gradient(left, #d0d0d0, #f1f1f1, #ffffff); + background: -ms-linear-gradient(left, #d0d0d0, #f1f1f1, #ffffff); + background: -o-linear-gradient(left, #d0d0d0, #f1f1f1, #ffffff); +} + +#content-section .sec-content { + position: relative; + padding: 10px 20px; +} + +.introduction .sec-content h2 { + font-size: 32px; + text-transform: uppercase; + margin-bottom: 10px; + font-weight: 600; + font-family: "Open Sans Condensed", sans-serif; + color: #246BFD; +} + +.introduction .sec-content h3 { + font-size: 22px; + text-transform: capitalize; + margin-bottom: 10px; + font-family: "Open Sans Condensed", sans-serif; +} + +.html-structure .sec-content h3, +.css-structure .sec-content h3, +.javascript .sec-content h3, +.sources-credits .sec-content h3, +.contact-form .sec-content h3, +.support .sec-content h3, +.map-settings .sec-content h3, +.count-timer .sec-content h3 { + font-size: 15px; + font-weight: 600; + margin-bottom: 30px; + line-height: 1.4em; +} + +.html-structure code { + font-size: 14px; + font-family: "Open Sans", sans-serif; + color: #353535; + padding: 30px 20px; + background: #f1f1f1; + height: auto; + display: block; +} + +.javascript ul li { + list-style: none; +} + +.sources-credits p { + margin-bottom: 2px; +} + +#content-section img { + display: inline-block; + max-width: 100%; + height: auto; +} + +#content-section .image-box { + position: relative; + padding: 0px; + margin: 30px 0px; +} + +.logo a { + font-size: 30px; + color: #ffffff; +} + +#Feature-card ul li { + list-style: disc; + margin-left: 22px; +} + +.setup { + display: block; + margin: 5px 0; + border: 1px solid #ddd; + padding: 20px; + border-radius: 10px; +} +.init { + padding-left: 20px; +} +.init li { + list-style: circle; +} +.setup li { + list-style: circle; +} diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/FontAwesome.otf b/submissions/valkey-ecommerce-demo/documentation/fonts/FontAwesome.otf new file mode 100644 index 0000000..f7936cc Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/FontAwesome.otf differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.eot b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..33b2bb8 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.eot differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.svg b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..1ee89d4 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.svg @@ -0,0 +1,565 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.ttf b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..ed9372f Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.ttf differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..8b280b9 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff2 b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..3311d58 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/fontawesome-webfont.woff2 differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.eot b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..b93a495 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.eot differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.svg b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..94fb549 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.ttf b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..1413fc6 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.ttf differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..9e61285 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff differ diff --git a/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff2 b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 0000000..64539b5 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/about.png b/submissions/valkey-ecommerce-demo/documentation/images/about.png new file mode 100644 index 0000000..ad037bc Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/about.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/blog.png b/submissions/valkey-ecommerce-demo/documentation/images/blog.png new file mode 100644 index 0000000..d2868b2 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/blog.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/contact.png b/submissions/valkey-ecommerce-demo/documentation/images/contact.png new file mode 100644 index 0000000..6095752 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/contact.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/contact_img.png b/submissions/valkey-ecommerce-demo/documentation/images/contact_img.png new file mode 100644 index 0000000..6daf069 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/contact_img.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/copyright.png b/submissions/valkey-ecommerce-demo/documentation/images/copyright.png new file mode 100644 index 0000000..e3f41ad Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/copyright.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/css.png b/submissions/valkey-ecommerce-demo/documentation/images/css.png new file mode 100644 index 0000000..75f955e Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/css.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/faq.png b/submissions/valkey-ecommerce-demo/documentation/images/faq.png new file mode 100644 index 0000000..04845eb Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/faq.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/favicon.png b/submissions/valkey-ecommerce-demo/documentation/images/favicon.png new file mode 100644 index 0000000..131a68a Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/favicon.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/footer.png b/submissions/valkey-ecommerce-demo/documentation/images/footer.png new file mode 100644 index 0000000..70c721b Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/footer.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/html-sample.png b/submissions/valkey-ecommerce-demo/documentation/images/html-sample.png new file mode 100644 index 0000000..d257dff Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/html-sample.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/logo.png b/submissions/valkey-ecommerce-demo/documentation/images/logo.png new file mode 100644 index 0000000..e351c69 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/logo.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/logo_img.png b/submissions/valkey-ecommerce-demo/documentation/images/logo_img.png new file mode 100644 index 0000000..ae813b1 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/logo_img.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/metatag.png b/submissions/valkey-ecommerce-demo/documentation/images/metatag.png new file mode 100644 index 0000000..18d6191 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/metatag.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/output.png b/submissions/valkey-ecommerce-demo/documentation/images/output.png new file mode 100644 index 0000000..9053efe Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/output.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/output3.png b/submissions/valkey-ecommerce-demo/documentation/images/output3.png new file mode 100644 index 0000000..a9c74f2 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/output3.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/output4.png b/submissions/valkey-ecommerce-demo/documentation/images/output4.png new file mode 100644 index 0000000..bf18452 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/output4.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/service.png b/submissions/valkey-ecommerce-demo/documentation/images/service.png new file mode 100644 index 0000000..a28cffe Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/service.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/team.png b/submissions/valkey-ecommerce-demo/documentation/images/team.png new file mode 100644 index 0000000..737ea27 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/team.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/images/testimonial.png b/submissions/valkey-ecommerce-demo/documentation/images/testimonial.png new file mode 100644 index 0000000..7b31a80 Binary files /dev/null and b/submissions/valkey-ecommerce-demo/documentation/images/testimonial.png differ diff --git a/submissions/valkey-ecommerce-demo/documentation/index.html b/submissions/valkey-ecommerce-demo/documentation/index.html new file mode 100644 index 0000000..bceaf4b --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/index.html @@ -0,0 +1,465 @@ + + + + + + Valkey - E-commerce React JS Template + + + + + + + + + + + + + +
+ + + +
+ +
+
+

Introduction

+
+ +
+
+

Valkey

+

Valkey - E-commerce React JS Template

+ +
+ + Created : May 19, 2026
+ Last Update : May 24, 2026
+ By : Roberto Luna-Rojas
+ +
+

+ Thank you for purchasing Valkey - E-commerce React JS Template + If you have any questions that are beyond the scope of + this help file, please feel free to email, via my user page contact form + or put a ticket at Support Center .
Thank you so much! +

+
+

All Pages

+
    +
  • 01) Home One
  • +
  • 02) Home Two
  • +
  • 03) Shop
  • +
  • 04) Product Details
  • +
  • 05) Product Details 2
  • +
  • 06) Cart
  • +
  • 07) Checkout
  • +
  • 08) Account
  • +
  • 09) Blog
  • +
  • 10) Blog Details
  • +
  • 11) Contact
  • +
  • 12) Wishlist
  • +
  • 13) Become a seller
  • +
  • 14) Vendor
  • +
  • 15) Vendor-details
  • +
  • 16) Vendor-two
  • +
  • 17) Vendor-two-details
  • +
  • 18) Vendor-two-details
  • + + + +
+
+

Setup Development Environment

+
+
    +
  • Install recommended version of NodeJS
  • +
  • Use this command line on the terminal to navigate to the project root directory.
  • +
  • Run those flowing commands. +
      + npm install
      + npm start + + +
    +
  • +
  • You will now see the website live.
  • +
  • Build the project using this command.
  • +
  • Run those flowing commands. +
      + npm run build + + +
    +
  • +
+
+
+
+ + +
+
+

React JsX Structure

+
+ +
+
+

+ The React js template uses Latest Bootstrap 5 with valid + HTML5 JSX tags. This theme is a responsive layout with 4 column Support + column. All of the information in content area is nested within a class + and comes with predefined classes. +

+ +
+ image html +
+
+
+
+

Output

+
+ image html +
+
+
+ + +
+
+

SASS Files and Structure

+
+ +
+
+

+ Mainly three main SCSS files are used in this theme. +

+

+ If you would like to edit a specific section of the site, simply find + the appropriate label in the SCSS file, and then scroll down until you + find the appropriate style that needs to be edited. +

+
+
+ image html +
+
+
+ + +
+
+

Customization

+
+ +
+
+

Change Meta Tag :

+ + +
+ + image html +
+
+
+
+

Change favicon :

+ + +
+ + image html +
+
+
+
+

Contact Form Structure :

+ + +
+ + image html +
+
+
+ + + + +
+
+

Logo Structure

+
+ +
+
+
+ image html +
+
+
+
+

Output

+
+
+ image html +
+
+
+
+
+ + +
+
+

App Structure

+
+
+
+ +
+
+ image html +
+
+ + +
+ + +
+
+

Component Area Structure

+
+ +
+
+
+ image html +
+
+
+ + +
+
+

Preloader Area Structure

+
+ +
+
+
+ image html +
+
+
+ + +
+
+

RouteScrollToTop Area Structure

+
+ +
+
+
+ image html +
+
+
+ + + + + +
+
+

blog Area Structure

+
+ +
+
+
+ image html +
+
+
+ + + + + + + +
+
+

Features

+
+ +
+
+
    +
  • SASS File Included
  • +
  • 100% pixel perfect design
  • +
  • Clean and Simple Style
  • +
  • Cross Browser Optimization.
  • +
  • Google font.
  • +
  • Compatible Browsers – IE9+, Chrome, Safari, Opera, Firefox
  • +
  • Documentation – Yes
  • +
  • License – GPL
  • +
  • Version – 1.0.0
  • +
  • Columns – 4
  • +
  • Files Included -html, JSX, scss
  • +
  • User Friendly Code
  • +
  • Easy to customize
  • +
  • W3C Validation
  • +
  • Clean & Modern Design
  • +
  • CSS3 animations
  • +
  • Full Responsive
  • +
  • SEO friendly code
  • +
+
+ + + +
+ + +
+
+

Sources and Credits

+
+ +
+
+

+ Fonts Used int the template are google fonts, you can find theme on + Google Fonts API +

+ +

Font Used:

+ + + + + +
+ Every Code is properly commented for Editing Ease. +
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/submissions/valkey-ecommerce-demo/documentation/js/bootstrap.min.js b/submissions/valkey-ecommerce-demo/documentation/js/bootstrap.min.js new file mode 100644 index 0000000..c6d3692 --- /dev/null +++ b/submissions/valkey-ecommerce-demo/documentation/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.3.2 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.2",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.2",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.2",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.2",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.2",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('