Una plataforma web moderna y responsiva para explorar, valorar y descubrir las mejores playas de las Islas Canarias.
- Descripción General
- Características Principales
- Tecnologías Utilizadas
- Arquitectura del Proyecto
- Instalación y Configuración
- Estructura del Proyecto
- Funcionalidades Clave
- Integraciones de API
- Componentes Principales
- Servicios
- Despliegue
- Equipo de Desarrollo
- Contribuir
- Licencia
Playea es una aplicación web full-stack que permite a los usuarios explorar, valorar y gestionar información sobre las playas de las Islas Canarias. La plataforma ofrece una experiencia completa con autenticación de usuarios, sistema de favoritos, comentarios y reseñas, visualización de mapas interactivos, datos meteorológicos en tiempo real y predicciones de mareas.
- Centralizar información sobre todas las playas canarias
- Facilitar la búsqueda mediante filtros avanzados y geolocalización
- Proporcionar datos en tiempo real de clima y mareas
- Crear una comunidad donde usuarios compartan experiencias
- Optimizar la experiencia en dispositivos móviles y de escritorio
- ✅ Registro e inicio de sesión con email/contraseña
- ✅ Autenticación con Google y Apple (integración OAuth)
- ✅ Recuperación de contraseña mediante OTP
- ✅ Perfiles de usuario personalizables (foto, nombre, apellido)
- ✅ Gestión de sesiones con Firebase Authentication
- ✅ Catálogo completo de 700+ playas canarias
- ✅ Búsqueda por nombre, isla o características
- ✅ Filtros avanzados (bandera azul, arena/roca, servicios)
- ✅ Ordenamiento por distancia del usuario
- ✅ Visualización en tarjetas responsivas con imágenes
- ✅ Comentarios y reseñas con calificación 1-5 estrellas
- ✅ Carga de imágenes en comentarios
- ✅ Edición y eliminación de propias reseñas
- ✅ Cálculo automático de calificación promedio
- ✅ Visualización de reseñas con timestamps relativos
- ✅ Integración con MapLibre GL para mapas interactivos
- ✅ Visualización de ubicación de playas con marcadores
- ✅ Cálculo de distancia desde ubicación del usuario
- ✅ Vista de mapa general con todas las playas
- ✅ Imágenes satelitales de alta calidad (MapTiler)
- ✅ Pronóstico del tiempo para 7 días (Open-Meteo API)
- ✅ Temperaturas máximas y mínimas
- ✅ Probabilidad de precipitación
- ✅ Códigos meteorológicos con íconos visuales
- ✅ Actualización automática de datos
- ✅ Datos de mareas del Instituto Hidrográfico de la Marina
- ✅ Gráficos interactivos con Chart.js
- ✅ Indicador de hora actual en gráfico
- ✅ Cálculo del puerto más cercano a cada playa
- ✅ Altura de marea en tiempo real
- ✅ Guardar playas favoritas por usuario
- ✅ Vista dedicada de playas guardadas
- ✅ Sincronización en tiempo real con Firebase
- ✅ Indicador visual en tarjetas de playas
- ✅ Ranking de playas con Bandera Azul
- ✅ Rankings por isla
- ✅ Ordenamiento por calificación de usuarios
- ✅ Visualización de estadísticas de ocupación
- Angular 19.1 - Framework web modular
- TypeScript 5.7 - Tipado estático
- Ionic 8.0 - Componentes UI móviles
- Tailwind CSS 4.1 - Estilos utility-first
- MapLibre GL 5.2 - Mapas interactivos
- Chart.js 4.4 - Gráficos de mareas
- ng2-charts 5.0 - Wrapper Angular para Chart.js
- ngx-sonner 3.1 - Notificaciones toast elegantes
- RxJS 7.8 - Programación reactiva
- date-fns 4.1 - Manipulación de fechas
- Axios 1.8 - Cliente HTTP
- Firebase 11.6 - Suite completa de backend
- Firestore - Base de datos NoSQL en tiempo real
- Authentication - Gestión de usuarios
- Storage - Almacenamiento de imágenes
- AngularFire2 5.4 - SDK Angular para Firebase
- Open-Meteo API - Datos meteorológicos gratuitos
- Instituto Hidrográfico de la Marina - Datos de mareas
- MapTiler - Imágenes satelitales para mapas
- Angular CLI 19.1 - Gestión de proyecto
- Karma + Jasmine - Testing unitario
- ESLint - Linting de código
- Capacitor 7.2 - Wrapper nativo para móviles
┌─────────────────────────────────────────────────┐
│ CLIENTE (Angular) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Pages │ │Components│ │ Services │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┴──────────────┘ │
│ │ │
└─────────────────────┼───────────────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Firebase │ │ Open │ │ IHM API │
│ Firestore│ │ Meteo │ │ (Mareas) │
└──────────┘ └──────────┘ └──────────┘
MVVM (Model-View-ViewModel) con inyección de dependencias:
- Models (
src/models/) - Interfaces TypeScript que definen la estructura de datos - Views (Templates HTML) - Presentación de la UI con data binding
- ViewModels (Componentes) - Lógica de presentación y estado
- Services (
src/services/) - Lógica de negocio y acceso a datos
// Ejemplo: Cargar playas
Component → Service → Firebase/API → Observable → Component → Template// beach.service.ts
getAllBeaches(): Observable<Beach[]> {
return this.firestore.collection<Beach>('beaches').valueChanges();
}
// home.component.ts
this.beachService.getAllBeaches().subscribe(beaches => {
this.beaches = beaches;
});- Node.js >= 18.x
- npm >= 9.x
- Angular CLI >= 19.x
- Cuenta de Firebase (Plan Spark o superior)
# 1. Clonar el repositorio
git clone https://github.com/your-repo/playea.git
cd playea/client
# 2. Instalar dependencias
npm install
# 3. Configurar variables de entorno
cp src/environments/environment.example.ts src/environments/environment.ts
# 4. Editar environment.ts con tus credenciales de Firebase// src/environments/environment.ts
export const environment = {
production: false,
firebaseConfig: {
apiKey: "TU_API_KEY",
authDomain: "TU_PROJECT.firebaseapp.com",
projectId: "TU_PROJECT_ID",
storageBucket: "TU_PROJECT.appspot.com",
messagingSenderId: "TU_SENDER_ID",
appId: "TU_APP_ID",
measurementId: "TU_MEASUREMENT_ID"
},
apiBaseUrl: 'http://localhost:4200'
};# Servidor de desarrollo (puerto 4200)
npm start
# o
ng serve
# Abrir en navegador
# http://localhost:4200# Build optimizado
npm run build
# Los archivos compilados estarán en dist/playa/browser/client/
├── public/ # Archivos estáticos
│ ├── images/ # Imágenes del sitio
│ └── mockup/ # Datos de prueba JSON
│
├── src/
│ ├── app/
│ │ ├── components/ # Componentes reutilizables
│ │ │ ├── beach-card/
│ │ │ ├── beach-comments/
│ │ │ ├── beach-detail-layout/
│ │ │ ├── beach-grid/
│ │ │ ├── category/
│ │ │ ├── comment-item/
│ │ │ ├── filter-panel/
│ │ │ ├── footer/
│ │ │ ├── header/
│ │ │ ├── maplibre-map/
│ │ │ ├── ranking-card/
│ │ │ ├── tides-status/
│ │ │ ├── user-header/
│ │ │ └── weather-display/
│ │ │
│ │ ├── layout/ # Componentes de diseño
│ │ │ ├── main/ # Layout con header/footer
│ │ │ └── noheader/ # Layout sin header
│ │ │
│ │ ├── models/ # Interfaces TypeScript
│ │ │ ├── beach.ts
│ │ │ ├── category.ts
│ │ │ ├── comment.ts
│ │ │ ├── user.ts
│ │ │ └── weather.ts
│ │ │
│ │ ├── pages/ # Páginas de la aplicación
│ │ │ ├── beachDetail/
│ │ │ ├── favourites/
│ │ │ ├── forgot-password/
│ │ │ ├── general-map/
│ │ │ ├── home/
│ │ │ ├── login/
│ │ │ ├── otp-verification/
│ │ │ ├── profile/
│ │ │ ├── ranking/
│ │ │ ├── rankingByIsland/
│ │ │ ├── register/
│ │ │ ├── search/
│ │ │ └── view-profile/
│ │ │
│ │ ├── services/ # Servicios de datos
│ │ │ ├── auth.service.ts
│ │ │ ├── beach.service.ts
│ │ │ ├── favourites.service.ts
│ │ │ ├── review.service.ts
│ │ │ ├── tide.service.ts
│ │ │ ├── user.service.ts
│ │ │ └── weather.service.ts
│ │ │
│ │ ├── utils/ # Utilidades
│ │ │ ├── toggle-password-view.ts
│ │ │ └── validators.ts
│ │ │
│ │ ├── app.component.ts
│ │ ├── app.config.ts
│ │ └── app.routes.ts
│ │
│ ├── environments/ # Configuración por entorno
│ ├── styles/ # Estilos globales
│ ├── index.html
│ ├── main.ts
│ └── server.ts # SSR (opcional)
│
├── angular.json # Configuración de Angular
├── package.json
├── tsconfig.json
├── tailwind.config.js # Configuración de Tailwind
└── README.md
Ubicación: src/app/pages/search/
// Búsqueda con filtros múltiples
onFiltersChange(filters: any) {
this.filters = {
hasLifeguard: filters.hasLifeguard,
hasSand: filters.hasSand,
hasRock: filters.hasRock,
hasShowers: filters.hasShowers,
hasToilets: filters.hasToilets,
hasFootShowers: filters.hasFootShowers
};
this.triggerSearch();
}
// Debounce para optimizar peticiones
this.searchSubject.pipe(
debounceTime(500),
switchMap(({ query, island, filters }) => {
return this.searchBeachService.searchBeaches(query, { island, ...filters });
})
).subscribe(beaches => this.beaches = beaches);Características:
- Búsqueda en tiempo real con debounce (500ms)
- Filtros por isla, servicios, tipo de arena
- Sincronización con URL query params
- Resultados paginados (opcional)
Ubicación: src/app/components/beach-card/
// Calcular distancia del usuario a la playa
private calculateDistance(
lat1: number, lon1: number,
lat2: number, lon2: number
): string {
const R = 6371; // Radio de la Tierra en km
const dLat = this.degToRad(lat2 - lat1);
const dLon = this.degToRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.degToRad(lat1)) * Math.cos(this.degToRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return `${distance.toFixed(2)} km`;
}Ubicación: src/app/components/beach-comments/
// Crear comentario con imagen
async onAddComment(): Promise<void> {
const formData = new FormData();
formData.append('beachId', this.beachId);
formData.append('rating', this.newCommentRating.toString());
formData.append('comment', this.newCommentText);
if (this.selectedImageFile) {
formData.append('image', this.selectedImageFile);
}
this.reviewService.createReview(formData).subscribe({
next: (createdReview) => {
this.reviews.push(createdReview.review);
toast.success('Comentario añadido correctamente');
},
error: (error) => toast.error('Error al añadir el comentario')
});
}Características:
- Subida de imágenes (max 5MB)
- Validación de tipo de archivo
- Previsualización antes de enviar
- Edición con opción de cambiar/eliminar imagen
Ubicación: src/app/components/maplibre-map/
private initMap(): void {
this.map = new maplibre.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/satellite/style.json?key=YOUR_KEY',
center: [this.longitude, this.latitude],
zoom: this.zoom,
attributionControl: false
});
// Añadir marcador de la playa
new maplibre.Marker()
.setLngLat([this.longitude, this.latitude])
.addTo(this.map);
}Ubicación: src/app/components/weather-display/
private fetchWeatherData(): void {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${this.latitude}&longitude=${this.longitude}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`;
fetch(url)
.then(response => response.json())
.then(data => {
this.weatherDays = data?.daily?.time.map((date: string, index: number) => ({
date,
tempMax: data.daily.temperature_2m_max[index],
tempMin: data.daily.temperature_2m_min[index],
precipitation: data.daily.precipitation_sum[index],
weathercode: data.daily.weathercode[index]
})) || [];
this.selectedDayIndex = 0;
});
}Códigos meteorológicos:
- 0: ☀️ Despejado
- 1-3: 🌤️⛅☁️ Nublado
- 45: 🌫️ Niebla
- 51-65: 🌧️ Lluvia
- 71-75: ❄️ Nieve
- 95: ⛈️ Tormenta
Ubicación: src/app/services/tide.service.ts
getTideData(latitude: number, longitude: number, date: string): Observable<TideData[]> {
const ports = this.portsSubject.getValue();
const nearestPort = this.findNearestPort(latitude, longitude, ports);
const formattedDate = date.replace(/-/g, '');
const url = `${this.apiUrl}?request=gettide&format=json&id=${nearestPort.id}&date=${formattedDate}`;
return this.http.get<TideResponse>(url).pipe(
map(response => {
const mareas = response.mareas?.datos?.marea || [];
return mareas.map(marea => ({
timestamp: new Date(`${response.mareas.fecha}T${marea.hora}:00Z`).toISOString(),
height: parseFloat(marea.altura)
})).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
})
);
}Características:
- Cálculo automático del puerto más cercano
- Gráfico interactivo con Chart.js
- Línea indicadora de hora actual
- Actualización cada minuto
- Datos del Instituto Hidrográfico de la Marina
Colecciones:
// beaches: Información de playas
{
id: string;
name: string;
slug: string;
island: string;
municipality: string;
coverUrl: string;
latitude: number;
longitude: number;
length: number;
blueFlag: boolean;
hasSand: boolean;
hasRock: boolean;
hasToilets: boolean;
hasShowers: boolean;
accessByCar: boolean;
accessByFoot: string | null;
annualMaxOccupancy: string | null;
classification: string;
environmentCondition: string | null;
grade?: number;
reviewsCount?: number;
}
// users: Información de usuarios
{
id: string;
firstName: string;
lastName: string;
email: string;
username: string;
avatarUrl: string;
createdAt: Date;
favorites: { [beachId: string]: boolean };
}
// reviews: Comentarios y valoraciones
{
id: number;
beachId: string;
userId: string;
rating: number;
comment: string;
imageUrl?: string;
createdAt: Date;
updatedAt: Date;
}Endpoint: https://api.open-meteo.com/v1/forecast
GET /forecast
?latitude={lat}
&longitude={lon}
&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode
&timezone=autoRespuesta:
{
"daily": {
"time": ["2025-01-28", "2025-01-29", ...],
"temperature_2m_max": [22.5, 23.1, ...],
"temperature_2m_min": [15.2, 16.0, ...],
"precipitation_sum": [0, 2.3, ...],
"weathercode": [0, 3, ...]
}
}Endpoint: https://ideihm.covam.es/api-ihm/getmarea
GET /getmarea
?request=gettide
&format=json
&id={portId}
&date={YYYYMMDD}Respuesta:
{
"mareas": {
"puerto": "Las Palmas",
"fecha": "2025-01-28",
"datos": {
"marea": [
{ "hora": "03:25", "altura": "2.45", "tipo": "pleamar" },
{ "hora": "09:50", "altura": "0.35", "tipo": "bajamar" }
]
}
}
}Propósito: Tarjeta individual de playa con imagen, nombre, isla y distancia.
@Component({
selector: 'app-beach-card',
standalone: true,
template: `
<a [href]="'/beach/' + beach.slug" class="beach-card__link">
<article class="beach-card">
<figure class="beach-card__media">
<img [src]="beach.coverUrl" [alt]="beach.name" />
<div *ngIf="beach.blueFlag" class="beach-card__flag--blue">
Bandera Azul
</div>
</figure>
<div class="beach-card__details">
<h3>{{ beach.name }}</h3>
<p>{{ beach.island }}</p>
<p *ngIf="distance">Estás a {{ distance }}</p>
</div>
</article>
</a>
`
})
export class BeachCardComponent implements OnInit {
@Input() beach: Beach | null = null;
distance: string | null = null;
ngOnInit() {
if (this.beach) this.getUserLocation();
}
}Propósito: Mostrar pronóstico del tiempo para 7 días.
@Component({
selector: 'app-weather-display',
template: `
<div class="weather-container">
<!-- Día seleccionado -->
<div *ngIf="weatherDays[selectedDayIndex]">
<div class="weather-icon">
{{ getWeatherIcon(weatherDays[selectedDayIndex].weathercode) }}
</div>
<p class="temperature">
{{ weatherDays[selectedDayIndex].tempMax }}°C
</p>
</div>
<!-- Selector de días -->
<div class="days-bar">
<button
*ngFor="let day of weatherDays; let i = index"
(click)="selectDay(i)"
[class.active]="i === selectedDayIndex">
{{ day.date | date:'EEE' }}
</button>
</div>
</div>
`
})
export class WeatherDisplayComponent {
@Input() latitude: number = 0;
@Input() longitude: number = 0;
weatherDays: DailyWeather[] = [];
selectedDayIndex: number = 0;
}Propósito: Gráfico de mareas del día.
@Component({
selector: 'app-tides-status',
template: `
<div class="tides-container">
<canvas
*ngIf="!isLoading"
baseChart
[data]="lineChartData"
[options]="lineChartOptions"
[type]="lineChartType">
</canvas>
</div>
`
})
export class TidesStatusComponent implements OnInit {
@Input() beach!: Beach;
lineChartData: ChartData<'line'> = {
datasets: [{
data: [],
label: 'Altura de la marea (m)',
borderColor: '#1e90ff'
}]
};
}Responsabilidad: Gestión de autenticación con Firebase.
@Injectable({ providedIn: 'root' })
export class AuthService {
async register(user: User) {
const userCredential = await createUserWithEmailAndPassword(
this._auth, user.email, user.password
);
await setDoc(doc(this._firestore, `Users/${userCredential.user.uid}`), {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
createdAt: new Date(),
imageUrl: user.imageUrl || 'default-avatar.png'
});
return userCredential;
}
async login(email: string, password: string) {
return await signInWithEmailAndPassword(this._auth, email, password);
}
async logout() {
return await signOut(this._auth);
}
}Responsabilidad: Acceso a datos de playas desde Firestore.
@Injectable({ providedIn: 'root' })
export class BeachService {
constructor(private firestore: AngularFirestore) {}
getAllBeaches(): Observable<Beach[]> {
return this.firestore.collection<Beach>('beaches').valueChanges();
}
getBeachBySlug(slug: string): Observable<Beach | undefined> {
return this.getAllBeaches().pipe(
map(beaches => beaches.find(b => b.slug === slug))
);
}
}Responsabilidad: Gestión de playas favoritas del usuario.
@Injectable({ providedIn: 'root' })
export class FavouritesService {
async addToFavourites(beachId: number): Promise<void> {
const userDocRef = doc(this.firestore, `Users/${userId}`);
await updateDoc(userDocRef, {
[`favorites.${beachId}`]: true
});
}
checkIfFavourite(beachId: number): Observable<boolean> {
const userDocRef = doc(this.firestore, `Users/${userId}`);
return from(getDoc(userDocRef)).pipe(
map(docSnap => docSnap.data()?.['favorites']?.[beachId] === true)
);
}
}Responsabilidad: CRUD de comentarios y reseñas.
@Injectable({ providedIn: 'root' })
export class ReviewService {
createReview(formData: FormData): Observable<any> {
return this.http.post(`${this.apiUrl}/reviews`, formData);
}
getReviewsForBeach(beachId: string): Observable<ReviewResponse> {
return this.http.get<ReviewResponse>(`${this.apiUrl}/reviews/beach/${beachId}`);
}
updateReview(reviewId: string, formData: FormData): Observable<any> {
return this.http.put(`${this.apiUrl}/reviews/${reviewId}`, formData);
}
deleteReview(reviewId: string): Observable<any> {
return this.http.delete(`${this.apiUrl}/reviews/${reviewId}`);
}
}# 1. Instalar Vercel CLI
npm i -g vercel
# 2. Login
vercel login
# 3. Desplegar
vercel --prodConfiguración (vercel.json):
{
"version": 2,
"builds": [{
"src": "package.json",
"use": "@vercel/static-build",
"config": { "distDir": "dist/playa/browser" }
}],
"routes": [
{ "handle": "filesystem" },
{ "src": "/(.*)", "dest": "/index.html" }
]
}# 1. Instalar Firebase CLI
npm install -g firebase-tools
# 2. Login
firebase login
# 3. Inicializar proyecto
firebase init hosting
# 4. Build y deploy
npm run build
firebase deploy --only hosting# Configurar en Vercel/Firebase
FIREBASE_API_KEY=your_api_key
FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
FIREBASE_PROJECT_ID=your_project_id
MAPTILER_API_KEY=your_maptiler_key- Miguel Ángel Rodríguez Ruano - Frontend Developer
- Gorka Eymard Santana Cabrera - Frontend Developer
- Sergio Acosta Quintana - Frontend Developer
| Desarrollador | Responsabilidades Principales |
|---|---|
| Miguel Ángel | Arquitectura, Firebase, Autenticación, Mapas |
| Gorka | UI/UX, Componentes, Diseño Responsivo, Tailwind |
| Sergio | Servicios, APIs Externas, Testing, Optimización |
- Scrum con sprints de 2 semanas
- Git Flow para control de versiones
- Code Review obligatorio antes de merge
- Figma para diseño y prototipos
- Trello para gestión de tareas
- Fork el repositorio
- Crea una rama feature (
git checkout -b feature/NuevaCaracteristica) - Commit tus cambios (
git commit -m 'Añadir nueva característica') - Push a la rama (
git push origin feature/NuevaCaracteristica) - Abre un Pull Request
// ✅ Buenas prácticas
// 1. Nombres descriptivos
const beachesWithBlueFlag = beaches.filter(b => b.blueFlag);
// 2. Tipos explícitos
function calculateDistance(lat1: number, lon1: number): string {
// ...
}
// 3. Comentarios útiles
// Calcula la distancia usando la fórmula de Haversine
const distance = haversineDistance(userLat, beachLat);
// 4. Manejo de errores
this.beachService.getAllBeaches().subscribe({
next: (beaches) => this.beaches = beaches,
error: (err) => {
console.error('Error loading beaches:', err);
toast.error('No se pudieron cargar las playas');
}
});- Componentes: PascalCase (
BeachCardComponent) - Servicios: PascalCase + Service (
BeachService) - Variables: camelCase (
beachList,selectedBeach) - Constantes: UPPER_SNAKE_CASE (
API_BASE_URL) - Archivos: kebab-case (
beach-card.component.ts)
tipo(alcance): descripción breve
[cuerpo opcional]
[pie opcional]
Tipos:
feat: Nueva característicafix: Corrección de bugdocs: Cambios en documentaciónstyle: Formato, punto y coma, etc.refactor: Refactorización de códigotest: Añadir/modificar testschore: Tareas de mantenimiento
Ejemplo:
feat(beach-detail): añadir gráfico de mareas
- Integrar Chart.js para visualización
- Conectar con API del IHM
- Añadir indicador de hora actual
Closes #42
Este proyecto está bajo la licencia MIT.
MIT License
Copyright (c) 2025 Playea Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- Repositorio: GitHub
- Documentación: Docs
- Figma: Diseños
- Trello: Tablero
- Email: contacto@playea.com
- Open-Meteo por la API meteorológica gratuita
- Instituto Hidrográfico de la Marina por los datos de mareas
- MapTiler por las imágenes satelitales
- Firebase por la infraestructura backend
- Ionic Team por los componentes UI
- Angular Team por el framework
- Sistema de notificaciones push
- Modo offline con Service Workers
- Compartir playas en redes sociales
- Rutas y direcciones a playas
- Chatbot de recomendaciones con IA
- Aplicación móvil nativa (Capacitor)
- Realidad aumentada para vistas 360°
- Integración con calendarios
- Sistema de badges y logros
- API pública para desarrolladores
- Marketplace de equipamiento de playa
- Reserva de sombrillas/hamacas
- Tours guiados virtuales
- Integración con hoteles cercanos
- Dashboard de analíticas avanzadas
Hecho con ❤️ por el equipo de Playea