diff --git a/index.html b/index.html
index 27a99f796e..e9194468ee 100644
--- a/index.html
+++ b/index.html
@@ -5,8 +5,12 @@
+
+
+
+
-
Hello Rigo
+ Expedition
diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py
deleted file mode 100644
index 88964176f1..0000000000
--- a/migrations/versions/0763d677d453_.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""empty message
-
-Revision ID: 0763d677d453
-Revises:
-Create Date: 2025-02-25 14:47:16.337069
-
-"""
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = '0763d677d453'
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('user',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('email', sa.String(length=120), nullable=False),
- sa.Column('password', sa.String(), nullable=False),
- sa.Column('is_active', sa.Boolean(), nullable=False),
- sa.PrimaryKeyConstraint('id'),
- sa.UniqueConstraint('email')
- )
- # ### end Alembic commands ###
-
-
-def downgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('user')
- # ### end Alembic commands ###
diff --git a/migrations/versions/a641ec06e1ec_.py b/migrations/versions/a641ec06e1ec_.py
new file mode 100644
index 0000000000..f0e4142b50
--- /dev/null
+++ b/migrations/versions/a641ec06e1ec_.py
@@ -0,0 +1,119 @@
+"""empty message
+
+Revision ID: a641ec06e1ec
+Revises:
+Create Date: 2026-04-08 18:19:57.987352
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'a641ec06e1ec'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('trip',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.String(length=30), nullable=False),
+ sa.Column('destination', sa.String(length=50), nullable=False),
+ sa.Column('state', sa.Enum('FINISHED', 'ONGOING', 'PLANNING', name='statetypes'), nullable=False),
+ sa.Column('starting_date', sa.Date(), nullable=False),
+ sa.Column('ending_date', sa.Date(), nullable=False),
+ sa.Column('budget', sa.Float(), nullable=False),
+ sa.Column('notes', sa.String(length=150), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('user',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=20), nullable=False),
+ sa.Column('last_name', sa.String(length=50), nullable=False),
+ sa.Column('email', sa.String(length=120), nullable=False),
+ sa.Column('password', sa.String(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email')
+ )
+ op.create_table('chat',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.String(length=50), nullable=True),
+ sa.Column('trip_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['trip_id'], ['trip.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('document',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.String(length=50), nullable=False),
+ sa.Column('url', sa.String(length=250), nullable=False),
+ sa.Column('trip_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['trip_id'], ['trip.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('expense',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('amount', sa.Float(), nullable=False),
+ sa.Column('description', sa.String(length=100), nullable=False),
+ sa.Column('trip_id', sa.Integer(), nullable=False),
+ sa.Column('payer_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['payer_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['trip_id'], ['trip.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('itinerary',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.String(length=30), nullable=False),
+ sa.Column('destination', sa.String(length=50), nullable=False),
+ sa.Column('hour', sa.Time(), nullable=False),
+ sa.Column('starting_date', sa.Date(), nullable=False),
+ sa.Column('notes', sa.String(length=150), nullable=False),
+ sa.Column('trip_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['trip_id'], ['trip.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('traveler',
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('trip_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['trip_id'], ['trip.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('user_id', 'trip_id')
+ )
+ op.create_table('debt',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('amount', sa.Float(), nullable=False),
+ sa.Column('debtor_id', sa.Integer(), nullable=False),
+ sa.Column('creditor_id', sa.Integer(), nullable=False),
+ sa.Column('expense_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['creditor_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['debtor_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['expense_id'], ['expense.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('message',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('content', sa.String(length=500), nullable=False),
+ sa.Column('date_time', sa.DateTime(), nullable=False),
+ sa.Column('chat_id', sa.Integer(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], ondelete='CASCADE'),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('message')
+ op.drop_table('debt')
+ op.drop_table('traveler')
+ op.drop_table('itinerary')
+ op.drop_table('expense')
+ op.drop_table('document')
+ op.drop_table('chat')
+ op.drop_table('user')
+ op.drop_table('trip')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/a9c11e29682c_.py b/migrations/versions/a9c11e29682c_.py
new file mode 100644
index 0000000000..9df1a222be
--- /dev/null
+++ b/migrations/versions/a9c11e29682c_.py
@@ -0,0 +1,36 @@
+"""empty message
+
+Revision ID: a9c11e29682c
+Revises: a641ec06e1ec
+Create Date: 2026-04-13 18:29:47.379931
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'a9c11e29682c'
+down_revision = 'a641ec06e1ec'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.alter_column('last_name',
+ existing_type=sa.VARCHAR(length=50),
+ nullable=True)
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('user', schema=None) as batch_op:
+ batch_op.alter_column('last_name',
+ existing_type=sa.VARCHAR(length=50),
+ nullable=False)
+
+ # ### end Alembic commands ###
diff --git a/package-lock.json b/package-lock.json
index 8d43d98ab7..398b3c017c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -877,19 +877,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
- "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -997,14 +984,6 @@
"@babel/types": "^7.20.7"
}
},
- "node_modules/@types/node": {
- "version": "16.11.12",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz",
- "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==",
- "dev": true,
- "optional": true,
- "peer": true
- },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -1307,14 +1286,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "optional": true,
- "peer": true
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -3915,29 +3886,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "optional": true,
- "peer": true,
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/source-map-support/node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@@ -4074,35 +4022,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/terser": {
- "version": "5.38.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz",
- "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true,
- "optional": true,
- "peer": true
- },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -4944,18 +4863,6 @@
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true
},
- "@jridgewell/source-map": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
- "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
- "dev": true,
- "optional": true,
- "peer": true,
- "requires": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
"@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@@ -5044,14 +4951,6 @@
"@babel/types": "^7.20.7"
}
},
- "@types/node": {
- "version": "16.11.12",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz",
- "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==",
- "dev": true,
- "optional": true,
- "peer": true
- },
"@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -5252,14 +5151,6 @@
"update-browserslist-db": "^1.1.1"
}
},
- "buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "optional": true,
- "peer": true
- },
"call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -6982,28 +6873,6 @@
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true
},
- "source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "optional": true,
- "peer": true,
- "requires": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true,
- "peer": true
- }
- }
- },
"string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
@@ -7094,30 +6963,6 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
- "terser": {
- "version": "5.38.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz",
- "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==",
- "dev": true,
- "optional": true,
- "peer": true,
- "requires": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "dependencies": {
- "commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "dev": true,
- "optional": true,
- "peer": true
- }
- }
- },
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
diff --git a/src/api/models.py b/src/api/models.py
index da515f6a1a..50851fbafd 100644
--- a/src/api/models.py
+++ b/src/api/models.py
@@ -1,19 +1,219 @@
+import enum
from flask_sqlalchemy import SQLAlchemy
-from sqlalchemy import String, Boolean
-from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy import String, Boolean, Enum, Date, Integer, ForeignKey, Float, Time, DateTime
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+from datetime import date, time, datetime
+from werkzeug.security import check_password_hash, generate_password_hash
db = SQLAlchemy()
+# --- ENUMS ---
+
+
+class StateTypes(enum.Enum):
+ FINISHED = "finished"
+ ONGOING = "ongoing"
+ PLANNING = "planning"
+
+
+class CategoryTypes(enum.Enum):
+ TRANSPORT = "transport"
+ LODGING = "lodging"
+ FOOD = "food"
+ ACTIVITIES = "activities"
+ OTHERS = "others"
+
+# --- MODELS ---
+
+
class User(db.Model):
+ __tablename__ = 'user'
id: Mapped[int] = mapped_column(primary_key=True)
- email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
+ name: Mapped[str] = mapped_column(String(20), nullable=False)
+ last_name: Mapped[str] = mapped_column(String(50), nullable=True)
+ email: Mapped[str] = mapped_column(
+ String(120), unique=True, nullable=False)
password: Mapped[str] = mapped_column(nullable=False)
- is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False)
+
+ # Relationships
+ travelers = relationship("Traveler", back_populates="users")
+ expenses_paid = relationship("Expense", back_populates="payers")
+ messages = relationship("Message", back_populates="authors")
+ debts_owed = relationship(
+ "Debt", foreign_keys="[Debt.debtor_id]", back_populates="debtors")
+ debts_to_receive = relationship(
+ "Debt", foreign_keys="[Debt.creditor_id]", back_populates="creditors")
+
+ def set_password(self, password: str) -> None:
+ self.password = generate_password_hash(password)
+
+ def check_password(self, password: str) -> bool:
+ return check_password_hash(self.password, password)
+
+ def serialize(self):
+ return {
+ "id": self.id,
+ "name": self.name,
+ "last_name": self.last_name,
+ "email": self.email
+ }
+
+
+class Trip(db.Model):
+ __tablename__ = 'trip'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(30), nullable=False)
+ destination: Mapped[str] = mapped_column(String(50), nullable=False)
+ state: Mapped[StateTypes] = mapped_column(
+ Enum(StateTypes), nullable=False)
+ starting_date: Mapped[date] = mapped_column(Date(), nullable=False)
+ ending_date: Mapped[date] = mapped_column(Date(), nullable=False)
+ budget: Mapped[float] = mapped_column(Float, nullable=False)
+ notes: Mapped[str] = mapped_column(String(150), nullable=True)
+
+ # Relationships
+ travelers = relationship("Traveler", back_populates="trips")
+ itineraries = relationship("Itinerary", back_populates="trips")
+ expenses = relationship("Expense", back_populates="trips")
+ documents = relationship("Document", back_populates="trips")
+ chats = relationship("Chat", back_populates="trips", uselist=False)
+
+ def serialize(self):
+ return {
+ "id": self.id,
+ "title": self.title,
+ "destination": self.destination,
+ "state": self.state.value,
+ "starting_date": str(self.starting_date),
+ "ending_date": str(self.ending_date),
+ "budget": self.budget,
+ "notes": self.notes
+ }
+
+ def serialize_common_trips(self):
+ return {
+ "id": self.id,
+ "title": self.title,
+ "state": self.state.value,
+ "starting_date": str(self.starting_date),
+ "ending_date": str(self.ending_date),
+ }
+
+
+
+class Traveler(db.Model):
+ __tablename__ = 'traveler'
+ user_id: Mapped[int] = mapped_column(ForeignKey(
+ "user.id", ondelete="CASCADE"), primary_key=True)
+ trip_id: Mapped[int] = mapped_column(ForeignKey(
+ "trip.id", ondelete="CASCADE"), primary_key=True)
+
+ users = relationship("User", back_populates="travelers")
+ trips = relationship("Trip", back_populates="travelers")
+
+ def serialize(self):
+ return {
+ "user_id": self.user_id,
+ "trip_id": self.trip_id
+ }
+
+ def serialize_trip(self):
+ return {
+ "trip_id": self.trip_id
+ }
+
+ def serialize_user(self):
+ return {
+ "user_id": self.user_id
+ }
+class Itinerary(db.Model):
+ __tablename__ = 'itinerary'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(30), nullable=False)
+ destination: Mapped[str] = mapped_column(String(50), nullable=False)
+ hour: Mapped[time] = mapped_column(Time, nullable=False)
+ starting_date: Mapped[date] = mapped_column(Date(), nullable=False)
+ notes: Mapped[str] = mapped_column(String(150), nullable=False)
+ trip_id: Mapped[int] = mapped_column(
+ ForeignKey("trip.id", ondelete="CASCADE"))
+
+ trips = relationship("Trip", back_populates="itineraries")
+
def serialize(self):
return {
"id": self.id,
- "email": self.email,
- # do not serialize the password, its a security breach
- }
\ No newline at end of file
+ "title": self.title,
+ "hour": str(self.hour),
+ "trip_id": self.trip_id
+ }
+
+
+class Expense(db.Model):
+ __tablename__ = 'expense'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ amount: Mapped[float] = mapped_column(Float, nullable=False)
+ description: Mapped[str] = mapped_column(String(100), nullable=False)
+ trip_id: Mapped[int] = mapped_column(
+ ForeignKey("trip.id", ondelete="CASCADE"))
+ payer_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+
+ trips = relationship("Trip", back_populates="expenses")
+ payers = relationship("User", back_populates="expenses_paid")
+ debts = relationship("Debt", back_populates="expenses")
+
+ def serialize(self):
+ return {"id": self.id, "amount": self.amount, "description": self.description}
+
+
+class Debt(db.Model):
+ __tablename__ = 'debt'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ amount: Mapped[float] = mapped_column(Float, nullable=False)
+ debtor_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+ creditor_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+ expense_id: Mapped[int] = mapped_column(ForeignKey("expense.id"))
+
+ expenses = relationship("Expense", back_populates="debts")
+ debtors = relationship("User", foreign_keys=[
+ debtor_id], back_populates="debts_owed")
+ creditors = relationship("User", foreign_keys=[
+ creditor_id], back_populates="debts_to_receive")
+
+
+class Document(db.Model):
+ __tablename__ = 'document'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(String(50), nullable=False)
+ url: Mapped[str] = mapped_column(String(250), nullable=False)
+ trip_id: Mapped[int] = mapped_column(
+ ForeignKey("trip.id", ondelete="CASCADE"))
+
+ trips = relationship("Trip", back_populates="documents")
+
+
+class Chat(db.Model):
+ __tablename__ = 'chat'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str] = mapped_column(
+ String(50), nullable=True) # <-- Título añadido
+ trip_id: Mapped[int] = mapped_column(
+ ForeignKey("trip.id", ondelete="CASCADE"))
+
+ trips = relationship("Trip", back_populates="chats")
+ messages = relationship("Message", back_populates="chats")
+
+
+class Message(db.Model):
+ __tablename__ = 'message'
+ id: Mapped[int] = mapped_column(primary_key=True)
+ content: Mapped[str] = mapped_column(String(500), nullable=False)
+ date_time: Mapped[datetime] = mapped_column(
+ DateTime, default=datetime.utcnow)
+ chat_id: Mapped[int] = mapped_column(
+ ForeignKey("chat.id", ondelete="CASCADE"))
+ user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
+
+ chats = relationship("Chat", back_populates="messages")
+ authors = relationship("User", back_populates="messages")
diff --git a/src/api/routes.py b/src/api/routes.py
index 029589a3a1..6b877dc23b 100644
--- a/src/api/routes.py
+++ b/src/api/routes.py
@@ -1,22 +1,118 @@
"""
This module takes care of starting the API Server, Loading the DB and Adding the endpoints
"""
-from flask import Flask, request, jsonify, url_for, Blueprint
-from api.models import db, User
-from api.utils import generate_sitemap, APIException
-from flask_cors import CORS
+from flask import Blueprint, jsonify, request
+from flask_jwt_extended import (
+ create_access_token,
+ get_jwt_identity,
+ jwt_required
+)
-api = Blueprint('api', __name__)
+from api.models import db, User, Trip, Traveler, Itinerary, Expense, Debt, Document, Chat, Message
+from api.utils import APIException
-# Allow CORS requests to this API
-CORS(api)
+api = Blueprint("api", __name__)
-@api.route('/hello', methods=['POST', 'GET'])
-def handle_hello():
- response_body = {
- "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request"
- }
+def get_json_payload():
+ return request.get_json(silent=True) or {}
- return jsonify(response_body), 200
+
+def build_auth_response(user, status_code, message):
+ access_token = create_access_token(identity=str(user.id))
+ return jsonify({
+ "message": message,
+ "access_token": access_token,
+ "user": user.serialize()
+ }), status_code
+
+
+def get_current_user():
+ identity = get_jwt_identity()
+ if identity is None:
+ raise APIException("Missing user identity in token", status_code=401)
+
+ try:
+ user_id = int(identity)
+ except (TypeError, ValueError) as error:
+ raise APIException("Invalid token identity",
+ status_code=401) from error
+
+ user = db.session.get(User, user_id)
+ if user is None:
+ raise APIException("Authenticated user was not found", status_code=404)
+
+ return user
+
+
+def validate_credentials(payload, require_name=False):
+ name = payload.get("name", "").strip()
+ email = payload.get("email", "").strip().lower()
+ password = payload.get("password", "")
+
+ if require_name and len(name) < 2:
+ raise APIException(
+ "Name must contain at least 2 characters", status_code=400)
+
+ if "@" not in email:
+ raise APIException(
+ "Please provide a valid email address", status_code=400)
+
+ if len(password) < 6:
+ raise APIException(
+ "Password must contain at least 6 characters", status_code=400)
+
+ return name, email, password
+
+
+@api.route("/login", methods=["POST"])
+@api.route("/signin", methods=["POST"])
+def sign_in():
+ data = get_json_payload()
+ _, email, password = validate_credentials(data)
+
+ user = User.query.filter_by(email=email).one_or_none()
+ if user is None or not user.check_password(password):
+ raise APIException("Wrong email or password", status_code=401)
+
+ return build_auth_response(user, 200, "Sign in successful")
+
+
+@api.route("/sign-up", methods=["POST"])
+@api.route("/signup", methods=["POST"])
+@api.route("/register", methods=["POST"])
+def sign_up():
+ data = get_json_payload()
+ name, email, password = validate_credentials(data, require_name=True)
+
+ existing_user = User.query.filter_by(email=email).one_or_none()
+ if existing_user is not None:
+ raise APIException(
+ "Ya existe un usuario con registrado con este correo", status_code=409)
+
+ new_user = User(
+ email=email,
+ name=name
+ )
+ new_user.set_password(password)
+
+ db.session.add(new_user)
+ db.session.commit()
+
+ return build_auth_response(new_user, 201, "Usuario creado correctamente")
+
+
+
+@api.route("/travels", methods=["GET"])
+@api.route("/trips", methods=["GET"])
+@jwt_required
+def travels():
+ user = get_current_user()
+ trips_by_traveler = Traveler.query.filter_by(user_id=user.id).all()
+ trip_ids = [t.trip_id for t in trips_by_traveler]
+ trips = Trip.query.filter(Trip.id.in_(trip_ids))
+
+ return jsonify({
+ "viajes": [trip.serialize_common_trips() for trip in trips]
+ }), 200
diff --git a/src/front/assets/img/EXPEDITION-LOGO.png b/src/front/assets/img/EXPEDITION-LOGO.png
new file mode 100644
index 0000000000..0de4fdf1a4
Binary files /dev/null and b/src/front/assets/img/EXPEDITION-LOGO.png differ
diff --git a/src/front/assets/img/fondo-landing.jpg b/src/front/assets/img/fondo-landing.jpg
new file mode 100644
index 0000000000..324f356358
Binary files /dev/null and b/src/front/assets/img/fondo-landing.jpg differ
diff --git a/src/front/assets/img/fondo-login.jpg b/src/front/assets/img/fondo-login.jpg
new file mode 100644
index 0000000000..e4474e9483
Binary files /dev/null and b/src/front/assets/img/fondo-login.jpg differ
diff --git a/src/front/assets/img/fondo-mytrips.jpg b/src/front/assets/img/fondo-mytrips.jpg
new file mode 100644
index 0000000000..99b8997493
Binary files /dev/null and b/src/front/assets/img/fondo-mytrips.jpg differ
diff --git a/src/front/components/Features.jsx b/src/front/components/Features.jsx
new file mode 100644
index 0000000000..e966b960aa
--- /dev/null
+++ b/src/front/components/Features.jsx
@@ -0,0 +1,24 @@
+import React from "react";
+
+export const Features = () => {
+ return (
+
+ Todo lo que necesitas
+
+
💰
+
Gestión de Gastos
+
Divide cuentas y liquida deudas fácilmente (Model: Expense/Debt).
+
+
+
📍
+
Itinerarios Vivos
+
Planifica cada parada con tu grupo en tiempo real (Model: Itinerary).
+
+
+
💬
+
Chat Grupal
+
Toda la comunicación en un solo lugar (Model: Chat/Message).
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/components/Hero.jsx b/src/front/components/Hero.jsx
new file mode 100644
index 0000000000..3588dd8803
--- /dev/null
+++ b/src/front/components/Hero.jsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { Stats } from "./Stats";
+
+export const Hero = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
Viaja en grupo sin complicaciones
+
+ {/* Texto actualizado al diseño */}
+
+ Organiza destinos, gastos y decisiones con tus amigos en un solo lugar.
+
+
+
+ {/* Botones actualizados */}
+ navigate("/login", { state: { tab: "register" } })}>
+ Comienza GRATIS
+
+ navigate("/demo")}>
+ Explorar cómo funciona
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx
index 30d43a2636..615237f6db 100644
--- a/src/front/components/Navbar.jsx
+++ b/src/front/components/Navbar.jsx
@@ -1,19 +1,87 @@
-import { Link } from "react-router-dom";
+import React, { useState } from "react";
+import { Link, useNavigate, useLocation } from "react-router-dom";
+import "../styles/Navbar.css";
+
+// 1. IMPORTAMOS TU LOGO AQUÍ
+// IMPORTANTE: Cambia "tu-logo.png" por el nombre exacto de tu archivo y asegúrate de que esté en esa carpeta.
+import logoExpedition from "../assets/img/EXPEDITION-LOGO.png";
export const Navbar = () => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ // MAGIA: Detectamos si el usuario está en la Landing Page (Inicio público)
+ const isLandingPage = location.pathname === "/";
+ // MAGIA NUEVA: Detectamos si está en el Login
+ const isLoginPage = location.pathname === "/login";
+
+ // Aquí agregamos la clase dinámica en el nav para que cambie a modo login
+ return (
+
+
+ {/* 1. IZQUIERDA: Menú Móvil y Logo */}
+
+
setMenuOpen(!menuOpen)}>
+
+
+
+ {/* 2. REEMPLAZAMOS EL TEXTO POR LA ETIQUETA
*/}
+
+
+
+
+
+ {/* 2. CENTRO: Solo se muestra si NO estamos en la Landing Page */}
+ {!isLandingPage && (
+
+
+ Inicio
+
+
+ Mis viajes
+
+
+ Perfil
+
+
+ )}
+
+ {/* 3. DERECHA: Cerrar sesión (condicional) y Perfil */}
+
+ {!isLandingPage && (
+
navigate("/login")}>
+ Cerrar sesión
+
+ )}
+
+
navigate("/login", { state: { tab: "login", key: Date.now() } })}
+ style={{ cursor: "pointer" }}
+ >
+
+
+
+
- return (
-
-
-
-
React Boilerplate
-
-
-
- Check the Context in action
-
-
-
-
- );
+ {/* Menú desplegable Móvil */}
+ {menuOpen && (
+
+ {!isLandingPage && setMenuOpen(false)}>Inicio}
+
+ setMenuOpen(false)}>Mis viajes
+
+ {!isLandingPage && setMenuOpen(false)}>Perfil}
+
+
+
+ {/* El botón cambia dependiendo de dónde estés */}
+ setMenuOpen(false)}>
+ {isLandingPage ? "Iniciar sesión" : "Cerrar sesión"}
+
+
+ )}
+
+ );
};
\ No newline at end of file
diff --git a/src/front/components/Stats.jsx b/src/front/components/Stats.jsx
new file mode 100644
index 0000000000..5a80fe5d9b
--- /dev/null
+++ b/src/front/components/Stats.jsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+export const Stats = () => {
+ return (
+
+
+
+ 1M+
+ Viajeros
+
+
+ 500K
+ Destinos
+
+
+ {/* El texto extra que aparece abajo en tu diseño */}
+
+ Expande tus horizontes, nosotros hacemos el resto
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/index.css b/src/front/index.css
index e69de29bb2..a6bc2543e8 100644
--- a/src/front/index.css
+++ b/src/front/index.css
@@ -0,0 +1,11 @@
+:root {
+ --brand-orange: #FF7A59;
+ --brand-navy: #1E3A5F;
+ --brand-teal: #2EC4B6;
+ --bg-light: #F8FAFC;
+}
+
+body {
+ font-family: 'Poppins', sans-serif;
+ color: var(--brand-navy);
+}
\ No newline at end of file
diff --git a/src/front/main.jsx b/src/front/main.jsx
index a5a3c781dc..433d93defb 100644
--- a/src/front/main.jsx
+++ b/src/front/main.jsx
@@ -1,29 +1,24 @@
+// src/front/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
-import './index.css' // Global styles for your application
-import { RouterProvider } from "react-router-dom"; // Import RouterProvider to use the router
-import { router } from "./routes"; // Import the router configuration
-import { StoreProvider } from './hooks/useGlobalReducer'; // Import the StoreProvider for global state management
+import './index.css'
+import { RouterProvider } from "react-router-dom";
+import { router } from "./routes";
+import { StoreProvider } from './hooks/useGlobalReducer';
import { BackendURL } from './components/BackendURL';
const Main = () => {
-
- if(! import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") return (
-
-
-
- );
+ // Si no hay URL de backend, mostramos el aviso
+ if(!import.meta.env.VITE_BACKEND_URL || import.meta.env.VITE_BACKEND_URL == "") {
+ return ;
+ }
+
return (
-
- {/* Provide global state to all components */}
-
- {/* Set up routing for the application */}
-
-
-
-
+
+ {/* RouterProvider DEBE ser el único que maneje la vista */}
+
+
);
}
-// Render the Main component into the root DOM element.
-ReactDOM.createRoot(document.getElementById('root')).render( )
+ReactDOM.createRoot(document.getElementById('root')).render( )
\ No newline at end of file
diff --git a/src/front/pages/AuthPage.jsx b/src/front/pages/AuthPage.jsx
new file mode 100644
index 0000000000..58433d3894
--- /dev/null
+++ b/src/front/pages/AuthPage.jsx
@@ -0,0 +1,112 @@
+import React, { useState, useEffect } from "react";
+import { useLocation } from "react-router-dom"; // <-- Importamos useLocation
+import "../styles/AuthPage.css";
+
+export const AuthPage = () => {
+ const location = useLocation();
+ const [isLogin, setIsLogin] = useState(true);
+
+ // Este efecto vigila de dónde viene el usuario para abrir la pestaña correcta
+ useEffect(() => {
+ if (location.state?.tab === "register") {
+ setIsLogin(false);
+ } else if (location.state?.tab === "login") {
+ setIsLogin(true);
+ }
+ }, [location]); // Se ejecuta cada vez que cambia la navegación
+
+ return (
+
+
+ {/* PESTAÑAS (TABS) */}
+
+ setIsLogin(true)}
+ >
+ Inicia sesión
+
+ setIsLogin(false)}
+ >
+ Regístrate
+
+
+
+ {/* CONTENIDO DEL FORMULARIO */}
+
+
{isLogin ? "Bienvenido de nuevo" : "Crea tu cuenta"}
+
+ {isLogin
+ ? "Gestiona tus aventuras compartidas con facilidad."
+ : "Únete a Expedition y empieza a planificar con tus amigos."}
+
+
+
+
+ {/* REDES SOCIALES */}
+
+ O CONTINÚA CON
+
+
+
+
+ Google
+
+
+ Facebook
+
+
+
+
+
+ {/* FOOTER DEL COMPONENTE */}
+
+ {isLogin ? "¿Aún no tienes cuenta? " : "¿Ya tienes una cuenta? "}
+ setIsLogin(!isLogin)}>
+ {isLogin ? "Únete a la comunidad" : "Inicia sesión"}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/pages/LandingPage.jsx b/src/front/pages/LandingPage.jsx
new file mode 100644
index 0000000000..0f75822b71
--- /dev/null
+++ b/src/front/pages/LandingPage.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { Hero } from "../components/Hero";
+import { Stats } from "../components/Stats";
+import { Features } from "../components/Features";
+import "../styles/LandingPage.css"; //
+
+export const LandingPage = () => {
+ return (
+
+ {/* Aquí llamamos a los componentes */}
+
+
+
+ {/* CALL TO ACTION FINAL (Este es pequeñito, lo podemos dejar aquí o separarlo luego) */}
+
+ ¿Listo para tu próxima expedición?
+ Crear mi viaje ahora
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx
index 9bfa31325c..300915cb2c 100644
--- a/src/front/pages/Layout.jsx
+++ b/src/front/pages/Layout.jsx
@@ -1,15 +1,17 @@
-import { Outlet } from "react-router-dom/dist"
-import ScrollToTop from "../components/ScrollToTop"
-import { Navbar } from "../components/Navbar"
-import { Footer } from "../components/Footer"
+import React from "react";
+import { Outlet } from "react-router-dom"; // Outlet es donde se cargan las páginas como LandingPage o Home
+import { Navbar } from "../components/Navbar"; // Verifica que la "N" coincida con tu archivo
+import { Footer } from "../components/Footer";
+import ScrollToTop from "../components/ScrollToTop";
-// Base component that maintains the navbar and footer throughout the page and the scroll to top functionality.
export const Layout = () => {
return (
-
-
-
-
-
- )
-}
\ No newline at end of file
+
+
+
+ {/* AQUÍ se inyectan las páginas */}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/pages/MyTrips.jsx b/src/front/pages/MyTrips.jsx
new file mode 100644
index 0000000000..f936744d9e
--- /dev/null
+++ b/src/front/pages/MyTrips.jsx
@@ -0,0 +1,162 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import "../styles/MyTrips.css";
+
+export const MyTrips = () => {
+ // 1. HERRAMIENTAS DE REACT
+ const navigate = useNavigate(); // Nos permite cambiar de página mediante código
+ const [activeFilter, setActiveFilter] = useState("Todos"); // Guarda qué botón de filtro está activo
+
+ // 2. BASE DE DATOS FAKE (Más adelante esto vendrá de Python/Base de datos)
+ const trips = [
+ {
+ id: 1,
+ title: "Lisboa Editorial",
+ date: "12 - 15 Septiembre",
+ status: "En curso",
+ image: "https://images.unsplash.com/photo-1555881400-74d7acaacd8b?auto=format&fit=crop&w=800&q=80",
+ featured: true,
+ },
+ {
+ id: 2,
+ title: "Costa Italiana",
+ date: "01 - 12 Octubre",
+ status: "Planificando",
+ image: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?auto=format&fit=crop&w=500&q=80",
+ featured: false,
+ },
+ {
+ id: 3,
+ title: "London Weekend",
+ date: "24 - 26 Noviembre",
+ status: "Planificando",
+ image: "https://images.unsplash.com/photo-1520939817895-060bdaf4fe1b?auto=format&fit=crop&w=500&q=80",
+ featured: false,
+ },
+ {
+ id: 4,
+ title: "Escapada a París",
+ date: "Julio 2023",
+ status: "Pasados",
+ image: "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&w=500&q=80",
+ featured: false,
+ }
+ ];
+
+ // 3. DATOS DE SUGERENCIAS
+ const suggestions = [
+ { title: "Misterios de la India", image: "https://images.unsplash.com/photo-1524492412937-b28074a5d7da?auto=format&fit=crop&w=400&q=80" },
+ { title: "Islas Griegas", image: "https://images.unsplash.com/photo-1533105079780-92b9be482077?auto=format&fit=crop&w=400&q=80" },
+ { title: "Tokio Moderno", image: "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?auto=format&fit=crop&w=400&q=80" },
+ { title: "Dubai Futurista", image: "https://images.unsplash.com/photo-1512453979798-5ea266f8880c?auto=format&fit=crop&w=400&q=80" }
+ ];
+
+ // 4. LA MAGIA DEL FILTRADO (¡LO QUE FALTABA!)
+ // Comprueba qué filtro está activo. Si es "Todos", guarda todos los viajes.
+ // Si no, filtra el array para que solo queden los que coincidan con el estado seleccionado.
+ const filteredTrips = activeFilter === "Todos"
+ ? trips
+ : trips.filter(trip => trip.status === activeFilter);
+
+ // 5. RENDERIZADO VISUAL
+ return (
+
+
+
+ {/* --- CABECERA Y BOTÓN DE NUEVO VIAJE --- */}
+
+
+
Mis Viajes
+
Gestiona tus próximas aventuras y recuerdos pasados.
+
+ {/* Al hacer clic, nos lleva a la página del formulario que acabamos de crear */}
+
navigate("/new-trip")}>
+ Crear nuevo viaje
+
+
+
+ {/* --- BOTONES DE FILTRO --- */}
+
+ {/* Generamos los botones dinámicamente desde un arreglo */}
+ {["Todos", "En curso", "Planificando", "Pasados"].map(filter => (
+ setActiveFilter(filter)}
+ >
+ {filter}
+
+ ))}
+
+
+ {/* --- CUADRÍCULA DE MIS VIAJES --- */}
+
+ {/* IMPORTANTE: Ahora mapeamos 'filteredTrips' en lugar del 'trips' original */}
+ {filteredTrips.map((trip) => (
+
+
+
+
+ {/* Genera la clase de color basada en el estado (ej: 'en-curso') */}
+
+ {trip.status}
+
+
+
+
+
{trip.title}
+
{trip.date}
+
+ {/* Si el viaje está "En curso", muestra la barra de progreso */}
+ {trip.status === "En curso" && (
+
+
+ {/* Botón para ir a ver los detalles específicos de este viaje (Paso 2 pendiente) */}
+
navigate(`/trip/${trip.id}`)}>
+ Ver detalles
+
+
+ )}
+
+
+
+ ))}
+
+ {/* --- TARJETA DE "EXPLORAR" (Siempre al final de la cuadrícula) --- */}
+
+
+
+
+
+
¿Sin ideas?
+
Explora destinos seleccionados por nuestra comunidad.
+
Explorar destinos
+
+
+
+
+ {/* --- SECCIÓN DE SUGERENCIAS INFERIOR --- */}
+
+
+
Sugerencias para ti
+ Ver todas
+
+
+ {suggestions.map((sug, index) => (
+
+
+
+
{sug.title}
+
+
+ ))}
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/pages/NewTrip.jsx b/src/front/pages/NewTrip.jsx
new file mode 100644
index 0000000000..03c0f211be
--- /dev/null
+++ b/src/front/pages/NewTrip.jsx
@@ -0,0 +1,111 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import "../styles/NewTrip.css"; // Crearemos este archivo en el Paso 2
+
+export const NewTrip = () => {
+ const navigate = useNavigate();
+
+ // Aquí guardaremos temporalmente lo que el usuario escriba
+ const [trip, setTrip] = useState({
+ title: "",
+ startDate: "",
+ endDate: "",
+ status: "Planificando", // Por defecto
+ imageUrl: ""
+ });
+
+ const handleChange = (e) => {
+ setTrip({ ...trip, [e.target.name]: e.target.value });
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ // Por ahora solo lo mostramos en consola. ¡Más adelante lo enviaremos a Python!
+ console.log("Viaje listo para guardar:", trip);
+
+ // Simulamos que se guardó y devolvemos al usuario a su panel
+ alert("¡Aventura creada con éxito! (Simulación)");
+ navigate("/my-trips");
+ };
+
+ return (
+
+
+
+ {/* Botón para volver atrás */}
+
navigate("/my-trips")}>
+ Volver a Mis Viajes
+
+
+
+
+
Planifica una nueva aventura
+
El primer paso de tu próximo gran viaje comienza aquí.
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/pages/TripDetails.jsx b/src/front/pages/TripDetails.jsx
new file mode 100644
index 0000000000..c87e6936c2
--- /dev/null
+++ b/src/front/pages/TripDetails.jsx
@@ -0,0 +1,126 @@
+import React, { useState } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+import "../styles/TripDetails.css";
+
+export const TripDetails = () => {
+ const { id } = useParams(); // Esto captura el número del viaje de la URL
+ const navigate = useNavigate();
+
+ // Pestañas internas
+ const [activeTab, setActiveTab] = useState("itinerario");
+
+ // Simulamos que buscamos los datos de este viaje específico en la base de datos
+ const trip = {
+ id: id,
+ title: "Lisboa Editorial",
+ dates: "12 - 15 Septiembre",
+ status: "En curso",
+ image: "https://images.unsplash.com/photo-1555881400-74d7acaacd8b?auto=format&fit=crop&w=1200&q=80",
+ budget: { total: 800, spent: 345 },
+ companions: ["Ana", "Carlos"],
+ itinerary: [
+ { day: 1, date: "12 Sept", title: "Llegada y Check-in", desc: "Vuelo de mañana. Tarde libre por Alfama." },
+ { day: 2, date: "13 Sept", title: "Ruta de los Miradores", desc: "Tour fotográfico y cena con Fado." },
+ { day: 3, date: "14 Sept", title: "Excursión a Sintra", desc: "Tren temprano para ver el Palacio da Pena." }
+ ]
+ };
+
+ return (
+
+
+ {/* CABECERA GIGANTE CON LA FOTO */}
+
+
+
navigate("/my-trips")}>
+ Volver
+
+
{trip.status}
+
{trip.title}
+
{trip.dates}
+
+
+
+ {/* CONTENIDO PRINCIPAL DIVIDIDO */}
+
+
+ {/* Columna Izquierda (Principal) */}
+
+
+ setActiveTab("itinerario")}>Itinerario
+ setActiveTab("gastos")}>Gastos
+ setActiveTab("documentos")}>Documentos
+
+
+ {activeTab === "itinerario" && (
+
+
Plan de Viaje
+
+ {trip.itinerary.map((item, index) => (
+
+
+
+
Día {item.day} - {item.date}
+
{item.title}
+
{item.desc}
+
+
+ ))}
+
+
Añadir actividad
+
+ )}
+
+ {activeTab === "gastos" && (
+
+
+
Aún no hay gastos registrados
+
Añade tus primeros tickets para llevar el control.
+
Añadir Gasto
+
+ )}
+
+ {activeTab === "documentos" && (
+
+
+
Carpeta vacía
+
Sube tus billetes de avión y reservas de hotel aquí.
+
+ )}
+
+
+ {/* Columna Derecha (Widgets de resumen) */}
+
+
+
Presupuesto
+
+
+ Gastado
+
{trip.budget.spent}€
+
+
+ Total
+
{trip.budget.total}€
+
+
+
+ {/* Calculamos el % de la barra matemáticamente */}
+
+
+
+
+
+
Viajeros
+
+ Yo
Tú (Organizador)
+ {trip.companions.map((friend, i) => (
+ {friend.charAt(0)}
{friend}
+ ))}
+
+
Invitar amigo
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/front/routes.jsx b/src/front/routes.jsx
index 0557df6141..6ce78857dd 100644
--- a/src/front/routes.jsx
+++ b/src/front/routes.jsx
@@ -9,6 +9,11 @@ import { Layout } from "./pages/Layout";
import { Home } from "./pages/Home";
import { Single } from "./pages/Single";
import { Demo } from "./pages/Demo";
+import { LandingPage } from "./pages/LandingPage";
+import { AuthPage } from "./pages/AuthPage"; // <-- Importamos la nueva página de Login/Registro
+import { MyTrips } from "./pages/MyTrips";
+import { NewTrip } from "./pages/NewTrip";
+import { TripDetails } from "./pages/TripDetails";
export const router = createBrowserRouter(
createRoutesFromElements(
@@ -19,12 +24,29 @@ export const router = createBrowserRouter(
// Note: The child paths of the Layout element replace the Outlet component with the elements contained in the "element" attribute of these child paths.
// Root Route: All navigation will start from here.
- } errorElement={Not found! } >
+ } errorElement={Not found! }>
+
+ {/* Landing Page es la vista por defecto al entrar a la web */}
+ } />
- {/* Nested Routes: Defines sub-routes within the BaseHome component. */}
- } />
- } /> {/* Dynamic route for single items */}
- } />
-
+ {/* Ruta para el sistema de Autenticación (Login/Registro) */}
+ } />
+
+ {/* Ruta para el sistema de Autenticación (mytrips) */}
+ } path="/my-trips" />
+
+ {/* Ruta para el sistema de Autenticación (newtrip) */}
+ } />
+
+ } />
+
+ {/* Cambiamos el path de Home a "/home" para que no choque con la Landing Page */}
+ } />
+
+ {/* Rutas dinámicas y de demostración */}
+ } />
+ } />
+
+
)
-);
\ No newline at end of file
+);
diff --git a/src/front/styles/AuthPage.css b/src/front/styles/AuthPage.css
new file mode 100644
index 0000000000..b276a6c5ba
--- /dev/null
+++ b/src/front/styles/AuthPage.css
@@ -0,0 +1,260 @@
+/* Contenedor principal corregido */
+.auth-wrapper {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ /* Fondo idéntico al de la Landing Page */
+ background: linear-gradient(rgba(46, 196, 182, 0.8), rgba(46, 196, 182, 0.6)),
+ url('../assets/img/fondo-login.jpg');
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ /* Aumentamos el padding superior a 100px para librar el Navbar (que mide 90px) */
+ padding: 100px 20px 40px 20px;
+ box-sizing: border-box;
+ font-family: 'Poppins', sans-serif;
+}
+
+/* La tarjeta blanca */
+.auth-card {
+ background: rgba(255, 255, 255, 0.98);
+ backdrop-filter: blur(5px);
+ width: 100%;
+ max-width: 450px;
+ border-radius: 16px;
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ /* --- AÑADE ESTAS DOS LÍNEAS --- */
+ position: relative;
+ z-index: 2000; /* Esto rompe el "escudo invisible" del Navbar */
+}
+
+/* Pestañas */
+.auth-tabs {
+ display: flex;
+ border-bottom: 1px solid #eaeaea;
+}
+
+.tab-btn {
+ flex: 1;
+ padding: 20px;
+ background: none;
+ border: none;
+ font-weight: 600;
+ font-size: 1rem;
+ color: #6c757d;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.tab-btn.active {
+ color: var(--brand-orange, #FF7A59); /* Naranja */
+ border-bottom: 3px solid var(--brand-orange, #FF7A59);
+}
+
+/* Cuerpo de la tarjeta */
+.auth-body {
+ padding: 30px 40px;
+ text-align: center;
+}
+
+.auth-body h2 {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--brand-navy, #1E3A5F); /* Azul Marino */
+ margin-bottom: 10px;
+}
+
+.auth-subtitle {
+ color: #6c757d;
+ font-size: 0.9rem;
+ margin-bottom: 30px;
+}
+
+/* Formularios e Inputs */
+.auth-form {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.input-group {
+ margin-bottom: 20px;
+ width: 100%;
+ text-align: left;
+}
+
+.input-group label {
+ display: block;
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--brand-navy, #1E3A5F); /* Azul Marino */
+ margin-bottom: 8px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.input-with-icon {
+ position: relative;
+ width: 100%;
+}
+
+.input-with-icon i {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #adb5bd;
+ z-index: 10;
+}
+
+.input-with-icon input {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 12px 12px 12px 45px;
+ border: 1px solid #ced4da;
+ border-radius: 10px;
+ font-size: 0.95rem;
+ font-family: 'Poppins', sans-serif;
+ outline: none;
+ transition: all 0.3s;
+}
+
+.input-with-icon input:focus {
+ border-color: var(--brand-teal, #2EC4B6); /* Borde Turquesa al escribir */
+ box-shadow: 0 0 0 0.2rem rgba(46, 196, 182, 0.15); /* Sombra Turquesa suave */
+}
+
+/* Enlace de contraseña y botón principal */
+.forgot-password {
+ text-align: right;
+ margin-top: -10px;
+ margin-bottom: 20px;
+}
+
+.forgot-password a {
+ font-size: 0.85rem;
+ color: var(--brand-teal, #2EC4B6); /* Turquesa */
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.btn-submit {
+ width: 100%;
+ background-color: var(--brand-orange, #FF7A59); /* Naranja */
+ color: white;
+ padding: 14px;
+ border: none;
+ border-radius: 10px;
+ font-weight: 700;
+ font-size: 1.05rem;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.btn-submit:hover {
+ background-color: #e56a4d;
+}
+
+/* Separador "O continúa con" */
+.auth-divider {
+ margin: 30px 0;
+ position: relative;
+ text-align: center;
+}
+
+.auth-divider::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ width: 100%;
+ height: 1px;
+ background-color: #eaeaea;
+ z-index: 1;
+}
+
+.auth-divider span {
+ background: rgba(255, 255, 255, 0.98);
+ padding: 0 15px;
+ color: #adb5bd;
+ font-size: 0.8rem;
+ font-weight: 500;
+ position: relative;
+ z-index: 2;
+}
+
+/* Botones sociales */
+.social-login {
+ display: flex;
+ gap: 15px;
+}
+
+.btn-social {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ padding: 12px;
+ background: white;
+ border: 1px solid #ced4da;
+ border-radius: 10px;
+ font-weight: 600;
+ color: var(--brand-navy, #1E3A5F);
+ cursor: pointer;
+ font-family: 'Poppins', sans-serif;
+ transition: background-color 0.3s;
+}
+
+.btn-social:hover {
+ background-color: #f8fafc;
+}
+
+/* Texto de abajo (Fuera de la tarjeta) */
+.auth-footer-text {
+ margin-top: 25px;
+ font-size: 0.95rem;
+ color: var(--brand-navy, #1E3A5F); /* Azul Marino */
+ font-weight: 500;
+}
+
+.toggle-link {
+ color: var(--brand-orange, #FF7A59); /* Naranja para resaltar la acción de cambiar */
+ font-weight: 700;
+ cursor: pointer;
+ text-decoration: none;
+}
+
+/* =========================================
+ ADAPTACIÓN PARA MÓVILES (Responsive)
+ ========================================= */
+@media (max-width: 500px) {
+ /* Reducimos el relleno interno de la tarjeta para que respire */
+ .auth-body {
+ padding: 30px 20px;
+ }
+
+ /* Hacemos las pestañas un pelín más pequeñas */
+ .tab-btn {
+ padding: 15px 10px;
+ font-size: 0.95rem;
+ }
+
+ /* Aseguramos que el texto del input NO se monte encima del icono */
+ .input-with-icon input {
+ padding-left: 45px !important;
+ }
+
+ /* El botón y los textos sociales un poco más compactos */
+ .btn-submit {
+ padding: 12px;
+ font-size: 1rem;
+ }
+
+ .btn-social {
+ font-size: 0.9rem;
+ }
+}
\ No newline at end of file
diff --git a/src/front/styles/LandingPage.css b/src/front/styles/LandingPage.css
new file mode 100644
index 0000000000..970c5164f6
--- /dev/null
+++ b/src/front/styles/LandingPage.css
@@ -0,0 +1,341 @@
+/* Contenedor principal: Evita que el contenido se salga de la pantalla en móviles */
+.landing-container {
+ width: 100%;
+ overflow-x: hidden;
+ background-color: var(--bg-light, #F8FAFC);
+ font-family: 'Poppins', sans-serif; /* Nueva tipografía principal */
+}
+
+/* =========================================
+ 1. HERO SECTION (Mobile First)
+ ========================================= */
+.hero-section {
+ /* Filtro turquesa semitransparente sobre la imagen */
+ background: linear-gradient(rgba(46, 196, 182, 0.8), rgba(46, 196, 182, 0.6)),
+ url('../assets/img/fondo-landing.jpg'); /* <-- Ruta a tu imagen local */
+ background-size: cover;
+ background-position: center;
+ min-height: 100vh; /* Ocupa toda la pantalla en móvil */
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 100px 20px 40px 20px; /* Padding reducido para celular */
+ text-align: left;
+ box-sizing: border-box;
+}
+
+.hero-content {
+ width: 100%;
+}
+
+.hero-section h1 {
+ font-size: 2.3rem; /* Tamaño adaptado a celular */
+ font-weight: 800;
+ line-height: 1.1;
+ margin-bottom: 16px;
+ color: white;
+ word-wrap: break-word;
+}
+
+.text-highlight {
+ color: var(--brand-orange, #FF7A59);
+}
+
+.hero-section p {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--brand-navy, #1E3A5F);
+ margin-bottom: 30px;
+ max-width: 100%;
+ padding: 0;
+}
+
+.hero-buttons {
+ display: flex;
+ flex-direction: column; /* Apilados en móvil */
+ gap: 12px;
+ width: 100%;
+}
+
+.btn-start, .btn-demo {
+ width: 100%; /* Ocupan todo el ancho en móvil */
+ padding: 14px 20px;
+ border-radius: 30px; /* Bordes de píldora */
+ font-weight: 700;
+ font-size: 1.05rem;
+ text-align: center;
+ border: none;
+ transition: all 0.3s ease;
+}
+
+.btn-start {
+ background-color: var(--brand-navy, #1E3A5F);
+ color: white;
+}
+
+.btn-start:hover {
+ background-color: #132742;
+}
+
+.btn-demo {
+ background-color: white;
+ color: var(--brand-navy, #1E3A5F);
+}
+
+.btn-demo:hover {
+ background-color: #f1f5f9;
+}
+
+/* =========================================
+ 2. STATS SECTION (Integrado en el Hero - Móvil)
+ ========================================= */
+.stats-wrapper {
+ margin-top: 40px;
+ width: 100%;
+ position: relative;
+ z-index: 10;
+}
+
+.stats-container {
+ display: flex;
+ justify-content: flex-start;
+ gap: 30px; /* Separación reducida para móvil */
+ margin-bottom: 15px;
+}
+
+.stat-item {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+}
+
+.stat-number {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: white;
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 600;
+}
+
+.hero-bottom-text {
+ color: white;
+ font-size: 1rem;
+ font-weight: 600;
+ font-style: italic;
+}
+
+/* =========================================
+ 3. FEATURES (Tarjetas)
+ ========================================= */
+.features-section {
+ padding: 40px 20px;
+}
+
+.features-section h2 {
+ text-align: center;
+ font-size: 1.6rem;
+ font-weight: 800;
+ margin-bottom: 24px;
+ color: var(--brand-navy, #1E3A5F);
+}
+
+.feature-card {
+ background: white;
+ padding: 24px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+ border: 1px solid #e2e8f0;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
+}
+
+.feature-card .icon {
+ font-size: 2rem;
+ margin-bottom: 12px;
+}
+
+.feature-card h3 {
+ font-size: 1.2rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+ color: var(--brand-navy, #1E3A5F);
+}
+
+.feature-card p {
+ color: #475569;
+ font-size: 0.95rem;
+ line-height: 1.5;
+}
+
+/* =========================================
+ 4. FOOTER / CALL TO ACTION
+ ========================================= */
+.cta-footer {
+ background-color: var(--brand-navy, #1E3A5F);
+ color: white;
+ padding: 50px 20px;
+ text-align: center;
+}
+
+.cta-footer h3 {
+ font-size: 1.5rem;
+ font-weight: 800;
+ margin-bottom: 24px;
+}
+
+.btn-final {
+ background-color: var(--brand-orange, #FF7A59);
+ color: white;
+ width: 100%;
+ padding: 16px;
+ border-radius: 14px;
+ font-weight: 700;
+ border: none;
+ font-size: 1.05rem;
+ transition: background-color 0.3s ease;
+}
+
+.btn-final:hover {
+ background-color: #e56a4d;
+}
+
+/* =========================================
+ ADAPTACIÓN PARA ESCRITORIO (PC/Tablets)
+ ========================================= */
+@media (min-width: 768px) {
+ /* Hero en PC */
+ .hero-section {
+ padding: 140px 20px 20px 40px;
+ min-height: 85vh;
+ }
+
+ .hero-section h1 {
+ font-size: 3.2rem;
+ max-width: 600px;
+ }
+
+ .hero-section p {
+ font-size: 1.15rem;
+ max-width: 500px;
+ }
+
+ /* Botones uno al lado del otro en PC */
+ .hero-buttons {
+ flex-direction: row;
+ max-width: 500px;
+ }
+
+ .btn-start, .btn-demo {
+ width: auto;
+ padding: 12px 30px;
+ }
+
+ /* Stats con más separación en PC */
+ .stats-container {
+ gap: 50px;
+ }
+
+ .stat-number {
+ font-size: 1.8rem;
+ }
+
+ .hero-bottom-text {
+ font-size: 1.1rem;
+ }
+
+ /* Opcional: Si quieres que los features se pongan en columnas en PC */
+ .features-section {
+ padding: 60px 40px;
+ }
+
+ .btn-final {
+ width: auto;
+ padding: 16px 40px;
+ }
+}
+
+
+/* =========================================
+ ADAPTACIÓN PARA ESCRITORIO (PC/Tablets)
+ ========================================= */
+@media (min-width: 768px) {
+ /* Hero en PC con Degradado Direccional */
+ .hero-section {
+ /* Fíjate en el "to right": oscuro a la izq, transparente a la der */
+ background: linear-gradient(to right, rgba(46, 196, 182, 0.95) 0%, rgba(46, 196, 182, 0.6) 50%, rgba(46, 196, 182, 0.1) 100%),
+ url('../assets/img/fondo-landing.jpg');
+ background-size: cover;
+ background-position: center;
+ min-height: 100vh;
+ padding: 160px 0 60px 0; /* Quitamos el padding lateral para controlarlo por dentro */
+ align-items: center; /* Centramos los contenedores internos */
+ }
+
+ /* CONTENEDORES ALINEADOS (La "línea invisible" con el Navbar) */
+ .hero-content, .stats-wrapper {
+ width: 100%;
+ max-width: 1200px; /* Exactamente el mismo ancho que tu Navbar */
+ margin: 0 auto;
+ padding: 0 20px; /* Exactamente el mismo padding que tu Navbar */
+ }
+
+ .stats-wrapper {
+ margin-top: 50px;
+ }
+
+ /* Textos Imponentes */
+ .hero-section h1 {
+ font-size: 5rem; /* Letras gigantes */
+ font-weight: 800;
+ letter-spacing: -2px; /* Letras más juntitas */
+ line-height: 1.05;
+ max-width: 850px;
+ }
+
+ .hero-section p {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-top: 20px;
+ margin-bottom: 40px;
+ max-width: 650px;
+ }
+
+ .hero-buttons {
+ flex-direction: row;
+ max-width: 600px;
+ gap: 20px;
+ }
+
+ .btn-start, .btn-demo {
+ width: auto;
+ padding: 14px 35px;
+ font-size: 1.15rem;
+ }
+
+ .stat-number {
+ font-size: 2.5rem;
+ }
+
+ .stat-label {
+ font-size: 1.3rem;
+ }
+
+ .hero-bottom-text {
+ font-size: 1.2rem;
+ max-width: 500px;
+ margin-top: 15px;
+ }
+
+ .features-section {
+ padding: 60px 40px;
+ }
+
+ .btn-final {
+ width: auto;
+ padding: 16px 40px;
+ }
+}
\ No newline at end of file
diff --git a/src/front/styles/MyTrips.css b/src/front/styles/MyTrips.css
new file mode 100644
index 0000000000..44944766dc
--- /dev/null
+++ b/src/front/styles/MyTrips.css
@@ -0,0 +1,353 @@
+/* =========================================
+ MAIN CONTAINER Y FONDO TURQUESA
+ ========================================= */
+.dashboard-wrapper {
+ min-height: 100vh;
+ font-family: 'Poppins', sans-serif;
+
+ /* ¡AQUÍ ESTÁ TU FONDO EXACTO DEL LOGIN Y LANDING! */
+ background: linear-gradient(rgba(46, 196, 182, 0.8), rgba(46, 196, 182, 0.6)),
+ url('../assets/img/fondo-mytrips.jpg');
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+
+ /* Texto general en blanco para contrastar con el turquesa */
+ color: #ffffff;
+}
+
+.dashboard-container {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 140px 20px 60px 20px;
+}
+
+/* =========================================
+ HEADER
+ ========================================= */
+.dashboard-header {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.dashboard-header h1 {
+ font-size: 2.2rem;
+ font-weight: 800;
+ margin-bottom: 5px;
+ letter-spacing: -1px;
+ color: #ffffff; /* Título blanco */
+}
+
+.dashboard-header p {
+ color: rgba(255, 255, 255, 0.9); /* Subtítulo blanco suave */
+ font-size: 0.95rem;
+ font-weight: 500;
+}
+
+/* Botón Blanco para que destaque */
+.btn-new-trip {
+ background-color: #ffffff;
+ color: var(--brand-teal, #2EC4B6);
+ padding: 14px 24px;
+ border-radius: 12px;
+ font-weight: 700;
+ font-size: 1rem;
+ border: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);
+}
+
+.btn-new-trip:hover {
+ background-color: var(--brand-navy, #1E3A5F);
+ color: white;
+}
+
+/* =========================================
+ FILTERS (TABS)
+ ========================================= */
+.trip-filters {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 30px;
+ overflow-x: auto;
+ padding-bottom: 5px;
+}
+
+/* Filtros transparentes con borde blanco */
+.btn-filter {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.5);
+ padding: 10px 20px;
+ border-radius: 30px;
+ color: #ffffff;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+}
+
+/* Filtro activo se rellena de blanco */
+.btn-filter.active {
+ background: #ffffff;
+ color: var(--brand-teal, #2EC4B6);
+ border-color: #ffffff;
+}
+
+/* =========================================
+ TRIPS GRID (Las tarjetas de viaje)
+ ========================================= */
+.trips-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px;
+ margin-bottom: 60px;
+}
+
+.trip-card {
+ background: white; /* La tarjeta se queda blanca */
+ color: var(--brand-navy, #1E3A5F); /* Texto oscuro por dentro */
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+ border: none;
+ display: flex;
+ flex-direction: column;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.trip-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
+}
+
+.trip-img-container {
+ position: relative;
+ height: 200px;
+}
+
+.trip-img-container img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.status-badge {
+ position: absolute;
+ top: 15px;
+ left: 15px;
+ padding: 6px 14px;
+ border-radius: 20px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: white;
+ letter-spacing: 0.5px;
+}
+
+/* Colores condicionales */
+.status-badge.en-curso { background-color: var(--brand-teal, #2EC4B6); }
+.status-badge.planificando { background-color: var(--brand-orange, #FF7A59); }
+.status-badge.pasados { background-color: #94a3b8; }
+
+.trip-info {
+ padding: 24px;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.trip-info h3 {
+ font-size: 1.3rem;
+ font-weight: 700;
+ margin-bottom: 10px;
+}
+
+.trip-info p {
+ color: #64748b;
+ font-size: 0.95rem;
+ font-weight: 500;
+}
+
+.trip-progress {
+ margin-top: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 20px;
+ border-top: 1px solid #f1f5f9;
+}
+
+.progress-bar {
+ flex-grow: 1;
+ height: 8px;
+ background: #f1f5f9;
+ border-radius: 4px;
+ margin-right: 15px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ width: 60%;
+ height: 100%;
+ background: var(--brand-teal, #2EC4B6);
+ border-radius: 4px;
+}
+
+.link-details {
+ font-size: 0.9rem;
+ font-weight: 700;
+ color: var(--brand-teal, #2EC4B6);
+ cursor: pointer;
+ transition: color 0.3s;
+}
+
+.link-details:hover {
+ color: var(--brand-orange, #FF7A59);
+}
+
+/* Explore Card adaptada al fondo turquesa */
+.explore-card {
+ border: 2px dashed rgba(255, 255, 255, 0.5);
+ background: transparent;
+ color: white;
+ box-shadow: none;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.explore-card:hover {
+ border-color: white;
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.compass-icon {
+ width: 65px;
+ height: 65px;
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.8rem;
+ margin: 0 auto 15px auto;
+}
+
+.btn-explore-link {
+ background: none;
+ border: none;
+ color: white;
+ font-weight: 700;
+ margin-top: 15px;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+/* =========================================
+ SUGGESTIONS SECTION
+ ========================================= */
+.suggestions-section {
+ margin-top: 40px;
+}
+
+.suggestions-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.suggestions-header h2 {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: white;
+}
+
+.link-view-all {
+ color: white;
+ font-weight: 700;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.suggestions-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 20px;
+}
+
+.suggestion-card {
+ position: relative;
+ border-radius: 12px;
+ overflow: hidden;
+ height: 300px;
+ cursor: pointer;
+ box-shadow: 0 10px 20px rgba(0,0,0,0.15);
+}
+
+.suggestion-card img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.5s;
+}
+
+.suggestion-card:hover img {
+ transform: scale(1.05);
+}
+
+.suggestion-overlay {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 30px 20px 20px 20px;
+ background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
+ color: white;
+}
+
+.suggestion-overlay h4 {
+ font-size: 1.1rem;
+ font-weight: 700;
+}
+
+/* =========================================
+ DESKTOP ADAPTATION
+ ========================================= */
+@media (min-width: 768px) {
+ .dashboard-header {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-end;
+ }
+
+ .dashboard-header h1 {
+ font-size: 2.8rem;
+ }
+
+ .btn-new-trip {
+ width: auto;
+ }
+
+ .trips-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .featured-card {
+ grid-column: span 2;
+ }
+
+ .featured-card .trip-img-container {
+ height: 280px;
+ }
+}
\ No newline at end of file
diff --git a/src/front/styles/Navbar.css b/src/front/styles/Navbar.css
new file mode 100644
index 0000000000..27e11f3384
--- /dev/null
+++ b/src/front/styles/Navbar.css
@@ -0,0 +1,201 @@
+.navbar-expedition {
+ width: 100%;
+ height: 90px;
+ background-color: transparent;
+ position: absolute;
+ top: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ font-family: 'Poppins', sans-serif;
+}
+
+.nav-container {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative; /* Importante para el centrado */
+}
+
+/* =========================================
+ IZQUIERDA Y LOGO RESTAURADO A TU MEDIDA
+ ========================================= */
+.nav-left {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.logo-img {
+ height: 200px; /* <--- TU MEDIDA ORIGINAL RESTAURADA */
+ width: auto;
+ max-width: 250px;
+ display: block;
+ object-fit: contain;
+}
+
+.nav-logo {
+ text-decoration: none;
+}
+
+.menu-btn {
+ background: none;
+ border: none;
+ font-size: 1.3rem;
+ color: var(--brand-navy, #1E3A5F);
+ cursor: pointer;
+}
+
+/* =========================================
+ CENTRO (Enlaces)
+ ========================================= */
+.nav-center {
+ display: flex;
+ gap: 30px;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%); /* Centrado matemático perfecto */
+}
+
+.nav-link {
+ text-decoration: none;
+ /* APLICAMOS EL AZUL DEL LOGO Y NEGRITA */
+ color: var(--brand-navy, #1E3A5F);
+ font-size: 1rem;
+ font-weight: 700; /* NEGRITA */
+ padding-bottom: 5px;
+ border-bottom: 2px solid transparent;
+ transition: all 0.3s ease;
+}
+
+.nav-link:hover {
+ color: var(--brand-orange, #FF7A59); /* Cambia a naranja al pasar el mouse */
+}
+
+.nav-link.active {
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 800; /* Extra negrita si estás en esa página */
+ border-bottom: 2px solid var(--brand-navy, #1E3A5F);
+}
+
+/* =========================================
+ DERECHA (Logout y Perfil)
+ ========================================= */
+.nav-right {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+}
+
+.logout-text {
+ font-size: 0.95rem;
+ /* APLICAMOS EL AZUL DEL LOGO Y NEGRITA */
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 700; /* NEGRITA */
+ cursor: pointer;
+ transition: color 0.3s;
+}
+
+.logout-text:hover {
+ color: var(--brand-orange, #FF7A59); /* Cambia a naranja al pasar el mouse */
+}
+
+.profile-icon {
+ width: 42px;
+ height: 42px;
+ background-color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--brand-navy, #1E3A5F);
+ border: 2px solid var(--brand-teal, #2EC4B6);
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+ transition: all 0.3s;
+}
+
+.profile-icon:hover {
+ background-color: var(--brand-teal, #2EC4B6);
+ color: white;
+}
+
+/* =========================================
+ MENÚ DESPLEGABLE MÓVIL
+ ========================================= */
+.dropdown-menu-mobile {
+ position: absolute;
+ top: 75px;
+ left: 20px;
+ right: 20px;
+ background: white;
+ border-radius: 12px;
+ padding: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ box-shadow: 0 10px 15px rgba(0,0,0,0.1);
+}
+
+.dropdown-menu-mobile a {
+ text-decoration: none;
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 600;
+ padding: 10px;
+ border-radius: 8px;
+ transition: background-color 0.3s;
+}
+
+.dropdown-menu-mobile a:hover {
+ background-color: #f1f5f9;
+ color: var(--brand-orange, #FF7A59);
+}
+
+/* =========================================
+ ADAPTACIÓN RESPONSIVA
+ ========================================= */
+.desktop-only {
+ display: none;
+}
+
+@media (min-width: 900px) {
+ .desktop-only {
+ display: flex;
+ }
+
+ .menu-btn {
+ display: none;
+ }
+}
+
+/* =========================================
+ MODO LOGIN (Aseguramos Azul y Negrita también aquí)
+ ========================================= */
+.navbar-login-mode .nav-link,
+.navbar-login-mode .logout-text {
+ /* APLICAMOS EL AZUL DEL LOGO Y NEGRITA PARA EL LOGIN */
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 700; /* NEGRITA */
+ text-shadow: none; /* Quitamos la sombra para que el azul se vea súper nítido */
+ opacity: 1;
+}
+
+.navbar-login-mode .nav-link:hover,
+.navbar-login-mode .logout-text:hover {
+ color: var(--brand-orange, #FF7A59); /* Naranja al pasar el ratón */
+}
+
+.navbar-login-mode .nav-link.active {
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 800; /* Extra negrita */
+ border-bottom: 2px solid var(--brand-navy, #1E3A5F); /* Línea azul debajo */
+}
+
+/* El botón de menú móvil lo mantenemos blanco en el login para que resalte sobre el fondo */
+.navbar-login-mode .menu-btn {
+ color: #ffffff;
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+}
\ No newline at end of file
diff --git a/src/front/styles/NewTrip.css b/src/front/styles/NewTrip.css
new file mode 100644
index 0000000000..4d3645c8b9
--- /dev/null
+++ b/src/front/styles/NewTrip.css
@@ -0,0 +1,139 @@
+/* Heredamos el fondo turquesa que ya usamos en MyTrips y Login */
+.new-trip-wrapper {
+ min-height: 100vh;
+ font-family: 'Poppins', sans-serif;
+ background: linear-gradient(rgba(46, 196, 182, 0.8), rgba(46, 196, 182, 0.6)),
+ url('../assets/img/fondo-login.jpg');
+ background-size: cover;
+ background-position: center;
+ background-attachment: fixed;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 120px 20px 40px 20px; /* Espacio superior para el Navbar */
+}
+
+.new-trip-container {
+ width: 100%;
+ max-width: 600px; /* Más estrecho que MyTrips porque es un formulario */
+}
+
+.btn-back {
+ background: transparent;
+ border: none;
+ color: white;
+ font-weight: 600;
+ font-size: 1rem;
+ cursor: pointer;
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: opacity 0.3s;
+}
+
+.btn-back:hover {
+ opacity: 0.8;
+}
+
+/* Tarjeta Blanca (Estilo Glassmorphism como el login) */
+.new-trip-card {
+ background: rgba(255, 255, 255, 0.98);
+ backdrop-filter: blur(5px);
+ border-radius: 16px;
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+}
+
+.card-header h2 {
+ color: var(--brand-navy, #1E3A5F);
+ font-weight: 800;
+ margin-bottom: 5px;
+ font-size: 1.8rem;
+}
+
+.card-header p {
+ color: #64748b;
+ font-size: 0.95rem;
+ margin-bottom: 30px;
+}
+
+/* Formularios */
+.new-trip-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.dates-row {
+ display: flex;
+ gap: 20px;
+}
+
+.dates-row .input-group {
+ flex: 1;
+}
+
+.input-group label {
+ display: block;
+ font-size: 0.8rem;
+ font-weight: 700;
+ color: var(--brand-navy, #1E3A5F);
+ margin-bottom: 8px;
+ text-transform: uppercase;
+}
+
+.input-group input,
+.input-group select {
+ width: 100%;
+ padding: 12px 15px;
+ border: 1px solid #ced4da;
+ border-radius: 10px;
+ font-family: 'Poppins', sans-serif;
+ font-size: 0.95rem;
+ box-sizing: border-box;
+ transition: all 0.3s;
+}
+
+.input-group input:focus,
+.input-group select:focus {
+ outline: none;
+ border-color: var(--brand-teal, #2EC4B6);
+ box-shadow: 0 0 0 0.2rem rgba(46, 196, 182, 0.15);
+}
+
+.input-group small {
+ display: block;
+ color: #94a3b8;
+ margin-top: 5px;
+ font-size: 0.8rem;
+}
+
+.btn-create-trip {
+ background-color: var(--brand-orange, #FF7A59);
+ color: white;
+ padding: 16px;
+ border: none;
+ border-radius: 10px;
+ font-weight: 700;
+ font-size: 1.1rem;
+ cursor: pointer;
+ margin-top: 10px;
+ transition: background-color 0.3s;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+}
+
+.btn-create-trip:hover {
+ background-color: #e56a4d;
+}
+
+/* Responsive */
+@media (max-width: 600px) {
+ .dates-row {
+ flex-direction: column;
+ gap: 20px;
+ }
+}
\ No newline at end of file
diff --git a/src/front/styles/TripDetails.css b/src/front/styles/TripDetails.css
new file mode 100644
index 0000000000..49a46cec88
--- /dev/null
+++ b/src/front/styles/TripDetails.css
@@ -0,0 +1,327 @@
+/* Contenedor general */
+.trip-details-wrapper {
+ min-height: 100vh;
+ font-family: 'Poppins', sans-serif;
+ background-color: #f8fafc; /* Fondo claro para que resalte la cabecera */
+}
+
+/* =========================================
+ HERO BANNER (Cabecera con foto)
+ ========================================= */
+.trip-hero {
+ height: 400px;
+ background-size: cover;
+ background-position: center;
+ position: relative;
+ display: flex;
+ align-items: flex-end;
+ padding-bottom: 50px;
+}
+
+.hero-content {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+ color: white;
+}
+
+.btn-back-light {
+ position: absolute;
+ top: 100px; /* Debajo del navbar */
+ left: 20px;
+ background: rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(5px);
+ border: none;
+ color: white;
+ padding: 10px 15px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: background 0.3s;
+}
+
+.btn-back-light:hover {
+ background: rgba(255, 255, 255, 0.4);
+}
+
+.hero-badge {
+ background: var(--brand-teal, #2EC4B6);
+ color: white;
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ display: inline-block;
+ margin-bottom: 10px;
+}
+
+.hero-content h1 {
+ font-size: 3rem;
+ font-weight: 800;
+ margin: 0 0 10px 0;
+ text-shadow: 0 2px 10px rgba(0,0,0,0.3);
+}
+
+.hero-content p {
+ font-size: 1.1rem;
+ font-weight: 500;
+}
+
+/* =========================================
+ ESTRUCTURA DE DOS COLUMNAS
+ ========================================= */
+.trip-content-container {
+ max-width: 1200px;
+ margin: -30px auto 50px auto; /* El -30px hace que se monte un poco sobre la foto (efecto moderno) */
+ padding: 0 20px;
+ display: grid;
+ grid-template-columns: 2fr 1fr; /* Columna izq el doble de ancha que la derecha */
+ gap: 30px;
+ position: relative;
+ z-index: 10;
+}
+
+/* =========================================
+ COLUMNA IZQUIERDA (Pestañas e Itinerario)
+ ========================================= */
+.main-column {
+ background: white;
+ border-radius: 16px;
+ padding: 30px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.05);
+}
+
+.content-tabs {
+ display: flex;
+ gap: 20px;
+ border-bottom: 2px solid #f1f5f9;
+ margin-bottom: 30px;
+}
+
+.content-tabs button {
+ background: none;
+ border: none;
+ padding: 10px 0;
+ font-size: 1.05rem;
+ font-weight: 600;
+ color: #64748b;
+ cursor: pointer;
+ position: relative;
+}
+
+.content-tabs button.active {
+ color: var(--brand-navy, #1E3A5F);
+}
+
+.content-tabs button.active::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background: var(--brand-orange, #FF7A59);
+ border-radius: 3px 3px 0 0;
+}
+
+/* Línea de tiempo (Itinerario) */
+.timeline {
+ border-left: 2px solid #e2e8f0;
+ margin-left: 10px;
+ padding-left: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+}
+
+.timeline-item {
+ position: relative;
+}
+
+.timeline-dot {
+ width: 14px;
+ height: 14px;
+ background: var(--brand-teal, #2EC4B6);
+ border-radius: 50%;
+ position: absolute;
+ left: -28px;
+ top: 5px;
+ box-shadow: 0 0 0 4px white;
+}
+
+.day-badge {
+ font-size: 0.8rem;
+ font-weight: 700;
+ color: var(--brand-orange, #FF7A59);
+ text-transform: uppercase;
+}
+
+.timeline-content h3 {
+ color: var(--brand-navy, #1E3A5F);
+ margin: 5px 0;
+ font-size: 1.2rem;
+}
+
+.timeline-content p {
+ color: #64748b;
+ font-size: 0.95rem;
+ margin: 0;
+}
+
+/* Estados Vacíos */
+.empty-state {
+ text-align: center;
+ padding: 50px 20px;
+ color: #94a3b8;
+}
+
+.empty-state i {
+ font-size: 3rem;
+ margin-bottom: 15px;
+ color: #cbd5e1;
+}
+
+.empty-state h3 {
+ color: var(--brand-navy, #1E3A5F);
+ margin-bottom: 5px;
+}
+
+/* =========================================
+ COLUMNA DERECHA (Widgets)
+ ========================================= */
+.side-column {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.summary-card {
+ background: white;
+ border-radius: 16px;
+ padding: 25px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.05);
+}
+
+.summary-card h3 {
+ color: var(--brand-navy, #1E3A5F);
+ margin: 0 0 20px 0;
+ font-size: 1.1rem;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.summary-card h3 i {
+ color: var(--brand-teal, #2EC4B6);
+}
+
+/* Presupuesto */
+.budget-numbers {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.budget-numbers span {
+ font-size: 0.85rem;
+ color: #64748b;
+}
+
+.budget-numbers h4 {
+ font-size: 1.5rem;
+ color: var(--brand-navy, #1E3A5F);
+ margin: 0;
+}
+
+.progress-bar-bg {
+ width: 100%;
+ height: 8px;
+ background: #f1f5f9;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progress-bar-fill {
+ height: 100%;
+ background: var(--brand-orange, #FF7A59);
+}
+
+/* Amigos */
+.friends-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 20px 0;
+}
+
+.friends-list li {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+ font-weight: 500;
+ color: var(--brand-navy, #1E3A5F);
+}
+
+.avatar {
+ width: 35px;
+ height: 35px;
+ background: var(--brand-teal, #2EC4B6);
+ color: white;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.8rem;
+ font-weight: 700;
+}
+
+.friend-avatar {
+ background: #cbd5e1;
+}
+
+/* Botones genéricos de acción */
+.btn-action, .btn-invite, .btn-add-day {
+ width: 100%;
+ padding: 12px;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.btn-action, .btn-add-day {
+ background: #f1f5f9;
+ color: var(--brand-navy, #1E3A5F);
+ margin-top: 20px;
+}
+
+.btn-invite {
+ background: transparent;
+ border: 1px dashed #cbd5e1;
+ color: var(--brand-teal, #2EC4B6);
+}
+
+.btn-action:hover, .btn-invite:hover, .btn-add-day:hover {
+ background: #e2e8f0;
+}
+
+/* =========================================
+ RESPONSIVE PARA MÓVILES
+ ========================================= */
+@media (max-width: 900px) {
+ .trip-content-container {
+ grid-template-columns: 1fr; /* Pasa a una sola columna */
+ margin-top: 20px;
+ }
+
+ .hero-content h1 {
+ font-size: 2.2rem;
+ }
+
+ .btn-back-light {
+ top: 80px;
+ }
+}
\ No newline at end of file