mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-07-15 17:43:36 +00:00
Compare commits
9 Commits
13-create-
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aed8e7311 | ||
|
|
90ba7588cc | ||
|
|
df3082cff5 | ||
| 6b7060450c | |||
|
|
f6e780fdf1 | ||
|
|
f427943ebc | ||
| aa98b2d169 | |||
| 5777437540 | |||
| e677170fd2 |
@@ -1,7 +1,9 @@
|
||||
services:
|
||||
redis:
|
||||
container_name: "PHANTOM-redis"
|
||||
image: redis:7-alpine
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Redis.dockerfile
|
||||
ports:
|
||||
- "${REDIS_PORT:-6378}:6379"
|
||||
volumes:
|
||||
@@ -9,7 +11,9 @@ services:
|
||||
restart: unless-stopped
|
||||
zookeeper:
|
||||
container_name: "PHANTOM-zookeeper"
|
||||
image: confluentinc/cp-zookeeper:latest
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Zookeeper.dockerfile
|
||||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ports:
|
||||
@@ -17,7 +21,9 @@ services:
|
||||
|
||||
kafka:
|
||||
container_name: "PHANTOM-kafka"
|
||||
image: confluentinc/cp-kafka:7.5.0
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Kafka.dockerfile
|
||||
depends_on:
|
||||
- zookeeper
|
||||
environment:
|
||||
@@ -36,7 +42,9 @@ services:
|
||||
|
||||
redpanda-console:
|
||||
container_name: "PHANTOM-redpanda-console"
|
||||
image: docker.redpanda.com/redpandadata/console:latest
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: RedpandaConsole.dockerfile
|
||||
depends_on:
|
||||
- kafka
|
||||
environment:
|
||||
|
||||
7
docker/Kafka.dockerfile
Normal file
7
docker/Kafka.dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM confluentinc/cp-kafka:7.5.0
|
||||
|
||||
# Expose Kafka ports
|
||||
# 9092: External client connections
|
||||
# 29092: Internal broker communication
|
||||
# 9999: JMX monitoring port
|
||||
EXPOSE 9092 29092 9999
|
||||
4
docker/Redis.dockerfile
Normal file
4
docker/Redis.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM redis:7-alpine
|
||||
|
||||
# Expose Redis port
|
||||
EXPOSE 6379
|
||||
4
docker/RedpandaConsole.dockerfile
Normal file
4
docker/RedpandaConsole.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM docker.redpanda.com/redpandadata/console:latest
|
||||
|
||||
# Expose Redpanda Console web UI port
|
||||
EXPOSE 8080
|
||||
4
docker/Zookeeper.dockerfile
Normal file
4
docker/Zookeeper.dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM confluentinc/cp-zookeeper:latest
|
||||
|
||||
# Expose Zookeeper client port
|
||||
EXPOSE 2181
|
||||
@@ -22,4 +22,3 @@
|
||||
(TeX-add-symbols
|
||||
'("footnotetextcopyrightpermission" 1)))
|
||||
:latex)
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
@phdthesis{,
|
||||
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
|
||||
the prices of products and services based on a number of dynamic factors. The aim of this thesis is to
|
||||
draw attention to the existence of these business practices, and the ethical and social implications that
|
||||
derive from them, and then focus on what could be effective solutions to increase the well-being of
|
||||
the community.
|
||||
In Chapter 2 of the thesis, a general introduction to the topic will be made, starting from its history
|
||||
and its evolution over the years; Chapter 3 will examine the different types of pricing algorithms.
|
||||
Subsequently, in Chapter 4 we will analyze the sectors in which they are most applicable, and the
|
||||
relative advantages and disadvantages they bring with them, with a critical analysis of the trade-offs
|
||||
generated. The effect of algorithmic pricing on competition will be studied, considering how the
|
||||
ability of algorithms to adapt quickly to market conditions can foster anti-competitive practices, such
|
||||
as price discrimination. Later, in Chapter 5, we will look at the issue of price transparency and how
|
||||
the opacity of algorithms can make it difficult for consumers to understand the pricing process and
|
||||
assess whether they are receiving fair treatment.
|
||||
To address these ethical issues, several possible solutions will be brought to light, described in
|
||||
Chapter 6, which will focus on the role of the government, as a regulatory, of the end consumer, who
|
||||
must be encouraged to educate and inform himself about the use of these practices, and of the
|
||||
company, as responsible for making its customers aware and acting in compliance with government
|
||||
laws, for fair and non-discriminatory use.},
|
||||
author = {Fabio Salassa and Paolo Pautassi},
|
||||
school = {Politecnico di Torino},
|
||||
title = {Politecnico di Torino Algorithmic Pricing in the digital age "Ethical considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" Tutor: Candidate},
|
||||
url = {https://webthesis.biblio.polito.it/secure/31375/1/tesi.pdf}
|
||||
}
|
||||
@inproceedings{Mueller2019,
|
||||
author = {Jonas W Mueller and Vasilis Syrgkanis and Matt Taddy},
|
||||
booktitle = {Advances in Neural Information Processing Systems 32 (NeurIPS 2019)},
|
||||
pages = {15442-15452},
|
||||
title = {Low-Rank Bandit Methods for High-Dimensional Dynamic Pricing},
|
||||
url = {https://proceedings.neurips.cc/paper/2019/file/0a3df70393993583a13c0dd6686f3f32-Paper.pdf},
|
||||
year = {2019}
|
||||
}
|
||||
@article{Prez-Ricardo2025,
|
||||
abstract = {The study aims to explore tourists' booking intentions by analyzing the price elasticity of demand in tourist accommodations. This analysis should reveal how changes in price affect booking behavior across different customer segments, using online booking records. A dataset was compiled from 106 hotels in Malaga, Spain, comprising 27,910 online bookings sourced exclusively from hotel websites. To understand the price elasticity of demand, a simple log-log regression was applied, segmenting the data based on key revenue-related variables. Subsequently, a cluster segmentation was performed using the Elbow method and K-means algorithm to identify distinct market segments. The findings highlighted that Family Travelers and Short Stay Travelers segments exhibited elastic demand, indicating higher sensitivity to price fluctuations. In contrast, Early Bookers and Mid-Season Long Stayers demonstrated inelastic demand, with lower responsiveness to changes in tourist accommodation prices. The number of variables analyzed in this study, along with the cluster analysis, represent a novelty and contribute to the existing literature on market segmentation and price elasticity of demand. This integration enriches both fields of research, offering mutual benefits and deeper insights that enhance the understanding of booking intention and pricing strategies.},
|
||||
author = {Elizabeth del Carmen Pérez-Ricardo and Josefa García-Mestanza},
|
||||
doi = {10.1016/j.iedeen.2025.100271},
|
||||
issn = {24448834},
|
||||
issue = {1},
|
||||
journal = {European Research on Management and Business Economics},
|
||||
keywords = {Booking intention,Price elasticity,Tourist segmentation},
|
||||
month = {1},
|
||||
publisher = {European Academy of Management and Business Economics},
|
||||
title = {Exploring booking intentions through price elasticity of demand in tourism accommodations using large-scale data analytics},
|
||||
volume = {31},
|
||||
year = {2025}
|
||||
}
|
||||
@article{ArnoudVdenBoer2015,
|
||||
author = {Arnoud V. den Boer},
|
||||
doi = {10.1016/j.sorms.2015.03.001},
|
||||
issue = {1},
|
||||
journal = {Surveys in Operations Research and Management Science},
|
||||
month = {6},
|
||||
pages = {1-18},
|
||||
title = {Dynamic pricing and learning: Historical origins, current research, and new directions},
|
||||
volume = {20},
|
||||
url = {https://www.sciencedirect.com/science/article/pii/S1876735415000021},
|
||||
year = {2015}
|
||||
}
|
||||
@article{Iliou2021,
|
||||
author = {Christos Iliou and Theodoros Kostoulas and Theodora Tsikrika and Vasilis Katos and Stefanos Vrochidis and Ioannis Kompatsiaris},
|
||||
doi = {10.1145/3447815},
|
||||
issue = {3},
|
||||
journal = {Digital Threats: Research and Practice},
|
||||
pages = {1-26},
|
||||
title = {Detection of Advanced Web Bots by Combining Web Logs with Mouse Behavioural Biometrics},
|
||||
volume = {2},
|
||||
url = {https://dl.acm.org/doi/10.1145/3447815},
|
||||
year = {2021}
|
||||
}
|
||||
@article{Amjad2017,
|
||||
abstract = { In this paper, the question of interest is estimating true demand of a product at a given store location and time period in the retail environment based on a single noisy and potentially censored observation. To address this question, we introduce a %non-parametric framework to make inference from multiple time series. Somewhat surprisingly, we establish that the algorithm introduced for the purpose of "matrix completion" can be used to solve the relevant inference problem. Specifically, using the Universal Singular Value Thresholding (USVT) algorithm [7], we show that our estimator is consistent: the average mean squared error of the estimated average demand with respect to the true average demand goes to 0 as the number of store locations and time intervals increase to $\infty$. We establish naturally appealing properties of the resulting estimator both analytically as well as through a sequence of instructive simulations. Using a real dataset in retail (Walmart), we argue for the practical relevance of our approach. },
|
||||
author = {Muhammad J. Amjad and Devavrat Shah},
|
||||
doi = {10.1145/3154489},
|
||||
issue = {2},
|
||||
journal = {Proceedings of the ACM on Measurement and Analysis of Computing Systems},
|
||||
month = {12},
|
||||
pages = {1-28},
|
||||
publisher = {Association for Computing Machinery (ACM)},
|
||||
title = {Censored Demand Estimation in Retail},
|
||||
volume = {1},
|
||||
url = {https://par.nsf.gov/servlets/purl/10066022},
|
||||
year = {2017}
|
||||
}
|
||||
@article{Calvano2018,
|
||||
author = {Emilio Calvano and Giacomo Calzolari and Vincenzo Denicolo and Sergio Pastorello},
|
||||
doi = {10.2139/ssrn.3304991},
|
||||
journal = {SSRN Electronic Journal},
|
||||
title = {Artificial Intelligence, Algorithmic Pricing and Collusion},
|
||||
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3304991},
|
||||
year = {2018}
|
||||
}
|
||||
@misc{gha_ffary_day_2025_amazon_perplexit,
|
||||
author = {Shirin Ghaffary and Matt Day},
|
||||
note = {Updated 2025-11-05},
|
||||
title = {Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff},
|
||||
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Mathematical formalization of agent-induced pricing distortions. Formal definition of potential loss mechanisms $\alpha D$
|
||||
|
||||
We consider a business across time during which we have an evolving vector $p_t \in \Re^N$ where $N$ is the number of products in our catalogue. our price vector is directly dependent on a demand function $q_t$ which we define as a linear method of a price elasticity matrix $B_t$. This is the same setup that Microsoft created in their research. \autocite{Mueller2019}
|
||||
We consider a business across time during which we have an evolving vector $p_t \in \Re^N$ where $N$ is the number of products in our catalogue. our price vector is directly dependent on a demand function $q_t$ which we define as a linear method of a price elasticity matrix $B_t$. This is the same setup that Microsoft created in their research.
|
||||
|
||||
We gether interaction data from users interacting with a sample platform simulating a hotel/airline which generates interaction distributions $I_t = \{(p_t, q_t^\text{obs}, \pi_t)\}_{t=1}^T$
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
\begin{abstract}
|
||||
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
|
||||
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
|
||||
\end{abstract}
|
||||
|
||||
\maketitle
|
||||
@@ -42,10 +42,10 @@
|
||||
\input{chapters/06-conclusion}
|
||||
|
||||
|
||||
|
||||
\onecolumn
|
||||
\printbibliography
|
||||
|
||||
\clearpage
|
||||
\onecolumn
|
||||
\appendix
|
||||
\input{../build/concatenated_code}
|
||||
|
||||
|
||||
@@ -1,36 +1,14 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
# Phantom Air/Hotels
|
||||
|
||||
First, run the development server:
|
||||
Design Discovery Documentation: https://github.com/velocitatem/PHANTOM/wiki/Design-Discovery
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
> This webapp serves two modes `{HOTEL,AIRLINE}` which are given by an env variable
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
The webapp should serve under the / route the landing page which for both platforms is very similar. We define a set of components like Hero, Card, Button, Link ... This we can then pass to specific components each mode might demand that makes it behave differently, hotel cards showing hotel rooms from database and airline cards showing flights from database and each fetching prices from the pricing provider with a different HTTP parameter.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
- globally we define a middleware.ts which is our switcher for modes.
|
||||
- /app will have (airline) and (hotel) children which each have a layout.tsx and page.tsx where /app also has a parent layout defining layout.tsx and globals.css for any shared styling to avoid repretition.
|
||||
- /components/ is gonna have ui/ which defines things like Button, Card, DatePicker with generic definitions and any tracking or observation code. We then define feats/airline/ and feats/hotel/ as children of components with specific components like AirlineHero and HotelCard.
|
||||
- in /styles/ we define airline.css and hotel.css to tailor accents and styling for each.
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f5f5;
|
||||
--text-primary: #333333;
|
||||
--text-secondary: #666666;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 32px;
|
||||
--border-radius: 8px;
|
||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -19,8 +28,71 @@
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 12px 24px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.section-spacing {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
302
web/src/styles/airline.css
Normal file
302
web/src/styles/airline.css
Normal file
@@ -0,0 +1,302 @@
|
||||
/* Airline Platform - Sky Blue Theme */
|
||||
|
||||
:root[data-mode="airline"] {
|
||||
--accent-primary: #007aff;
|
||||
--accent-secondary: #4caf50;
|
||||
--accent-warning: #ff3b30;
|
||||
--accent-primary-hover: #0051d5;
|
||||
--accent-primary-light: #e6f2ff;
|
||||
--text-accent: #007aff;
|
||||
}
|
||||
|
||||
[data-mode="airline"] {
|
||||
--primary-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .btn-primary {
|
||||
background-color: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .btn-primary:hover {
|
||||
background-color: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--accent-primary);
|
||||
border: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .btn-secondary:hover {
|
||||
background-color: var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .badge-value {
|
||||
background-color: var(--accent-secondary);
|
||||
color: #ffffff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .badge-warning {
|
||||
background-color: var(--accent-warning);
|
||||
color: #ffffff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .search-form {
|
||||
background: var(--bg-primary);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-card {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 3fr 2fr;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: var(--spacing-md);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.15);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-timing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-time {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-airport {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-route {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-duration {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-stops {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .flight-pricing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .fare-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .fare-option:hover {
|
||||
border-color: var(--accent-primary);
|
||||
background-color: var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .fare-class {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .fare-price {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-price-bar {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md) 0;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-option {
|
||||
min-width: 100px;
|
||||
padding: var(--spacing-sm);
|
||||
text-align: center;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-option:hover {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-option.active {
|
||||
border-color: var(--accent-primary);
|
||||
background-color: var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .date-price {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .progress-wizard {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
margin: var(--spacing-lg) auto;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: #e0e0e0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step.active .wizard-number {
|
||||
background: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step.completed .wizard-number {
|
||||
background: var(--accent-secondary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .wizard-step.active .wizard-label {
|
||||
color: var(--accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="airline"] a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-mode="airline"] a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .input-field {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .input-field:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .filter-sidebar {
|
||||
background: var(--bg-primary);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .filter-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .filter-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="airline"] .checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-mode="airline"] .checkbox-label:hover {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
400
web/src/styles/hotel.css
Normal file
400
web/src/styles/hotel.css
Normal file
@@ -0,0 +1,400 @@
|
||||
/* Hotel Platform - Action Blue Theme */
|
||||
|
||||
:root[data-mode="hotel"] {
|
||||
--accent-primary: #007aff;
|
||||
--accent-secondary: #4caf50;
|
||||
--accent-warning: #d9534f;
|
||||
--accent-primary-hover: #0051d5;
|
||||
--accent-primary-light: #e6f2ff;
|
||||
--text-accent: #007aff;
|
||||
--bg-tertiary: #f5f5f7;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] {
|
||||
--primary-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .btn-primary {
|
||||
background-color: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .btn-primary:hover {
|
||||
background-color: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--accent-primary);
|
||||
border: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .btn-secondary:hover {
|
||||
background-color: var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .badge-value {
|
||||
background-color: var(--accent-secondary);
|
||||
color: #ffffff;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .badge-warning {
|
||||
background-color: var(--accent-warning);
|
||||
color: #ffffff;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .badge-rating {
|
||||
background-color: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .search-form {
|
||||
background: var(--bg-primary);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-card {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr auto;
|
||||
gap: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: var(--spacing-md);
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.15);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .image-carousel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .carousel-nav {
|
||||
position: absolute;
|
||||
bottom: var(--spacing-sm);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .carousel-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .carousel-dot.active {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-info {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-location {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .rating-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .feature-tag {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .hotel-pricing {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-amount {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-unit {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-original {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
margin-right: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .urgency-message {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-warning);
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .free-cancellation {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-secondary);
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .filter-sidebar {
|
||||
background: var(--bg-primary);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .filter-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .filter-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .filter-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .checkbox-label:hover {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .checkbox-count {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .price-slider {
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .slider-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .slider-range {
|
||||
height: 100%;
|
||||
background: var(--accent-primary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .slider-values {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .map-toggle {
|
||||
background: var(--bg-primary);
|
||||
border: 2px solid var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .map-toggle:hover {
|
||||
background: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .sort-dropdown {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .sort-dropdown:focus {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .view-toggle {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .view-button {
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .view-button.active {
|
||||
background: var(--accent-primary);
|
||||
color: #ffffff;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .input-field {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .input-field:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--accent-primary-light);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .tab-navigation {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .tab-item {
|
||||
padding: 12px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .tab-item:hover {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
[data-mode="hotel"] .tab-item.active {
|
||||
color: var(--accent-primary);
|
||||
border-bottom-color: var(--accent-primary);
|
||||
}
|
||||
Reference in New Issue
Block a user