Initial commit: MAX PWA app with crash simulation
- PWA support for iOS and Android installation - Phone number input with validation - Two types of error modals (crash and network error) - Dark/light theme support - Service Worker for offline functionality - Complete icon set and manifest configuration
55
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# macOS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
89
README.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# MAX - PWA приложение
|
||||
|
||||

|
||||
|
||||
## 📱 Описание
|
||||
|
||||
MAX - это Progressive Web App (PWA) приложение, имитирующее проблемное поведение мобильного приложения с различными типами ошибок и крашей.
|
||||
|
||||
## ✨ Особенности
|
||||
|
||||
- 🎯 **PWA поддержка** - можно устанавливать на iOS и Android
|
||||
- 📱 **Адаптивный дизайн** - работает на всех устройствах
|
||||
- 🌙 **Темная/светлая тема** - автоматическое переключение
|
||||
- 💥 **Симуляция крашей** - различные типы ошибок
|
||||
- 🔔 **Push уведомления** - уведомления о сбоях
|
||||
- 📴 **Офлайн режим** - работает без интернета
|
||||
|
||||
## 🚀 Установка
|
||||
|
||||
### На iOS (iPhone/iPad):
|
||||
1. Откройте приложение в Safari
|
||||
2. Нажмите кнопку "Поделиться" (⎙)
|
||||
3. Выберите "На экран Домой"
|
||||
4. Подтвердите установку
|
||||
|
||||
### На Android:
|
||||
1. Откройте приложение в Chrome
|
||||
2. Нажмите на предложение "Установить приложение"
|
||||
3. Подтвердите установку
|
||||
|
||||
## 🎮 Функциональность
|
||||
|
||||
### Автоматический краш
|
||||
- Через 3 секунды после загрузки появляется модальное окно с ошибкой
|
||||
- iOS-стиль дизайн с кодом ошибки `EXC_BAD_ACCESS (0x4A7B9C2E)`
|
||||
|
||||
### Ввод номера телефона
|
||||
- Поле с маской для российских номеров
|
||||
- Кнопка активируется при вводе полного номера (10 цифр)
|
||||
- При нажатии "Продолжить" появляется второе окно ошибки
|
||||
|
||||
### Второе окно ошибки
|
||||
- Ошибка сети с кодом `NET_TIMEOUT (0x2F8A1B3C)`
|
||||
- Кнопки "Повторить" и "Отмена"
|
||||
- При повторе показывается та же ошибка
|
||||
|
||||
## 🛠 Технологии
|
||||
|
||||
- **HTML5** - семантическая разметка
|
||||
- **CSS3** - современные стили и анимации
|
||||
- **JavaScript** - интерактивность и PWA функции
|
||||
- **Service Worker** - кэширование и офлайн режим
|
||||
- **Web App Manifest** - PWA конфигурация
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
errormax/
|
||||
├── icons/ # Иконки приложения
|
||||
│ ├── max_logo.webp # Основной логотип
|
||||
│ └── icon-*.png # Иконки разных размеров
|
||||
├── index.html # Главная страница
|
||||
├── manifest.json # PWA манифест
|
||||
├── sw.js # Service Worker
|
||||
└── README.md # Документация
|
||||
```
|
||||
|
||||
## 🎨 Дизайн
|
||||
|
||||
- **Цветовая схема**: Фиолетовый градиент (#667eea → #764ba2)
|
||||
- **Шрифт**: SF Pro / Inter
|
||||
- **Стиль**: iOS-подобный дизайн
|
||||
- **Анимации**: Плавные переходы и эффекты
|
||||
|
||||
## 🔧 Разработка
|
||||
|
||||
Для локального запуска:
|
||||
|
||||
1. Клонируйте репозиторий
|
||||
2. Откройте `index.html` в браузере
|
||||
3. Для PWA функций используйте локальный сервер
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
MIT License
|
||||
|
||||
## 👨💻 Автор
|
||||
|
||||
Создано для демонстрации PWA технологий и симуляции проблемного поведения приложений.
|
||||
BIN
icons/app_icon_background.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
icons/icon-120.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/icon-128.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
icons/icon-144.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
icons/icon-152.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
icons/icon-180.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
icons/icon-192.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
icons/icon-384.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
icons/icon-72.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
icons/icon-96.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
10
icons/icon.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="80" ry="80" fill="url(#grad1)"/>
|
||||
<text x="256" y="280" font-family="Arial, sans-serif" font-size="120" font-weight="bold" text-anchor="middle" fill="white">MAX</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
BIN
icons/max_logo.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/original_logo.webp
Normal file
|
After Width: | Height: | Size: 310 KiB |
832
index.html
Normal file
|
|
@ -0,0 +1,832 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MAX</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#667eea">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="MAX">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="MAX">
|
||||
<meta name="msapplication-TileColor" content="#667eea">
|
||||
<meta name="msapplication-TileImage" content="icons/icon-144.png">
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="icons/icon-180.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="icons/icon-152.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="icons/icon-120.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="icons/icon-72.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="icons/icon-72.png">
|
||||
<link rel="apple-touch-icon" href="icons/icon-180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/icon-72.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/icon-72.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png">
|
||||
<link rel="shortcut icon" href="icons/icon-72.png">
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Light theme (default) */
|
||||
body {
|
||||
background: linear-gradient(180deg, #E8E5FF 0%, #F5F3FF 100%);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.phone-input-container {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.country-selector {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.phone-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.privacy-notice {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.country-code {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url('icons/max_logo.webp');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.phone-input-container {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.country-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 12px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.country-code {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.phone-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bottom-section {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: #007AFF;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.continue-button:hover {
|
||||
background: #0056CC;
|
||||
}
|
||||
|
||||
.continue-button:disabled {
|
||||
background: #666 !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
.privacy-notice {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.privacy-link {
|
||||
color: #007AFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.privacy-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* iOS-style modal */
|
||||
.ios-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.ios-modal-content {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 14px;
|
||||
padding: 0;
|
||||
max-width: 320px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ios-modal-header {
|
||||
padding: 20px 20px 10px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ios-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.ios-modal-message {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ios-modal-body {
|
||||
padding: 15px 20px;
|
||||
text-align: center;
|
||||
background: rgba(248, 248, 248, 0.8);
|
||||
}
|
||||
|
||||
.ios-modal-error-code {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ios-modal-buttons {
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ios-modal-button {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.ios-modal-button:first-child {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.ios-modal-button:last-child {
|
||||
color: #FF3B30;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ios-modal-button:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme modal adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ios-modal-content {
|
||||
background: rgba(44, 44, 46, 0.95);
|
||||
}
|
||||
|
||||
.ios-modal-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.ios-modal-message {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.ios-modal-body {
|
||||
background: rgba(28, 28, 30, 0.8);
|
||||
}
|
||||
|
||||
.ios-modal-error-code {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ios-modal-buttons {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ios-modal-button:first-child {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ios-modal-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo-container">
|
||||
<div class="logo-icon"></div>
|
||||
<span class="logo-text">MAX</span>
|
||||
</div>
|
||||
<div class="help-icon">?</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<h1 class="title">С каким номером<br>телефона хотите войти?</h1>
|
||||
<p class="subtitle">На него придёт СМС с кодом</p>
|
||||
|
||||
<div class="phone-input-container">
|
||||
<div class="country-selector">
|
||||
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAyNCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjYiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHk9IjYiIHdpZHRoPSIyNCIgaGVpZ2h0PSI2IiBmaWxsPSIjMDA1MkZGIi8+CjxyZWN0IHk9IjEyIiB3aWR0aD0iMjQiIGhlaWdodD0iNiIgZmlsbD0iI0ZGMDAwMCIvPgo8L3N2Zz4K" alt="RU" class="flag">
|
||||
<span class="country-code">+7</span>
|
||||
<svg class="dropdown-arrow" width="12" height="8" viewBox="0 0 12 8" fill="none">
|
||||
<path d="M1 1L6 6L11 1" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="tel" class="phone-input" placeholder="123 456-78-90" maxlength="13">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-section">
|
||||
<button class="continue-button">Продолжить</button>
|
||||
|
||||
<div class="privacy-notice">
|
||||
Нажимая «Продолжить», вы принимаете <a href="#" class="privacy-link">политику конфиденциальности</a> и <a href="#" class="privacy-link">пользовательское соглашение</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- iOS-style crash modal -->
|
||||
<div class="ios-modal" id="crashModal">
|
||||
<div class="ios-modal-content">
|
||||
<div class="ios-modal-header">
|
||||
<div class="ios-modal-title">MAX перестал работать</div>
|
||||
<div class="ios-modal-message">Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения</div>
|
||||
</div>
|
||||
<div class="ios-modal-body">
|
||||
<div class="ios-modal-error-code">Код ошибки: EXC_BAD_ACCESS (0x4A7B9C2E)</div>
|
||||
</div>
|
||||
<div class="ios-modal-buttons">
|
||||
<button class="ios-modal-button" onclick="sendReport()">Отправить отчёт</button>
|
||||
<button class="ios-modal-button" onclick="closeCrashModal()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second error modal -->
|
||||
<div class="ios-modal" id="secondErrorModal">
|
||||
<div class="ios-modal-content">
|
||||
<div class="ios-modal-header">
|
||||
<div class="ios-modal-title">Что-то пошло не так</div>
|
||||
<div class="ios-modal-message">Не удалось отправить СМС. Проверьте подключение к интернету и попробуйте снова</div>
|
||||
</div>
|
||||
<div class="ios-modal-body">
|
||||
<div class="ios-modal-error-code">Код ошибки: NET_TIMEOUT (0x2F8A1B3C)</div>
|
||||
</div>
|
||||
<div class="ios-modal-buttons">
|
||||
<button class="ios-modal-button" onclick="retryAction()">Повторить</button>
|
||||
<button class="ios-modal-button" onclick="closeSecondError()">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Service Worker Registration
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
.then(registration => console.log('SW registered'))
|
||||
.catch(error => console.log('SW registration failed'));
|
||||
}
|
||||
|
||||
// Handle theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
function handleThemeChange(e) {
|
||||
// Update theme-color meta tag
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (e.matches) {
|
||||
// Dark theme
|
||||
themeColorMeta.content = '#1a1a2e';
|
||||
} else {
|
||||
// Light theme
|
||||
themeColorMeta.content = '#667eea';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for theme changes
|
||||
mediaQuery.addListener(handleThemeChange);
|
||||
|
||||
// Set initial theme
|
||||
handleThemeChange(mediaQuery);
|
||||
|
||||
// Auto crash simulation after page load
|
||||
setTimeout(() => {
|
||||
showCrashModal();
|
||||
|
||||
// Show iOS-style notification
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([200, 100, 200]);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Phone input functionality
|
||||
const phoneInput = document.querySelector('.phone-input');
|
||||
const continueButton = document.querySelector('.continue-button');
|
||||
|
||||
// Set button as disabled initially
|
||||
continueButton.disabled = true;
|
||||
continueButton.style.background = '#666';
|
||||
|
||||
// Phone number formatting
|
||||
function formatPhoneNumber(value) {
|
||||
// Remove all non-digits
|
||||
const digits = value.replace(/\D/g, '');
|
||||
|
||||
// Format as XXX XXX-XX-XX (10 digits total)
|
||||
if (digits.length <= 3) {
|
||||
return digits;
|
||||
} else if (digits.length <= 6) {
|
||||
return digits.slice(0, 3) + ' ' + digits.slice(3);
|
||||
} else if (digits.length <= 8) {
|
||||
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6);
|
||||
} else if (digits.length <= 10) {
|
||||
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6, 8) + '-' + digits.slice(8);
|
||||
} else {
|
||||
// Limit to 10 digits
|
||||
return digits.slice(0, 3) + ' ' + digits.slice(3, 6) + '-' + digits.slice(6, 8) + '-' + digits.slice(8, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Phone input event listeners
|
||||
phoneInput.addEventListener('input', function(e) {
|
||||
const formatted = formatPhoneNumber(e.target.value);
|
||||
e.target.value = formatted;
|
||||
|
||||
// Enable/disable continue button based on full phone number
|
||||
const digits = formatted.replace(/\D/g, '');
|
||||
console.log('Digits entered:', digits, 'Length:', digits.length); // Debug info
|
||||
|
||||
if (digits.length === 10) {
|
||||
continueButton.disabled = false;
|
||||
continueButton.style.background = '#007AFF';
|
||||
continueButton.style.opacity = '1';
|
||||
console.log('Button activated!'); // Debug info
|
||||
} else {
|
||||
continueButton.disabled = true;
|
||||
continueButton.style.background = '#666';
|
||||
continueButton.style.opacity = '0.6';
|
||||
console.log('Button disabled, need', (10 - digits.length), 'more digits'); // Debug info
|
||||
}
|
||||
});
|
||||
|
||||
// Second error simulation after phone input
|
||||
continueButton.addEventListener('click', function() {
|
||||
if (!this.disabled) {
|
||||
const phoneNumber = phoneInput.value.replace(/\D/g, '');
|
||||
console.log('Phone number:', '+7' + phoneNumber);
|
||||
|
||||
// Show loading state
|
||||
this.textContent = 'Отправляем СМС...';
|
||||
this.disabled = true;
|
||||
this.style.background = '#666';
|
||||
|
||||
// Simulate second error after 2 seconds
|
||||
setTimeout(() => {
|
||||
showSecondError();
|
||||
|
||||
// Reset button
|
||||
this.textContent = 'Продолжить';
|
||||
this.disabled = false;
|
||||
this.style.background = '#007AFF';
|
||||
|
||||
// Vibration if supported
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([200, 100, 200]);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// iOS-style crash modal functions
|
||||
function showCrashModal() {
|
||||
document.getElementById('crashModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Second error modal functions
|
||||
function showSecondError() {
|
||||
document.getElementById('secondErrorModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeSecondError() {
|
||||
document.getElementById('secondErrorModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function retryAction() {
|
||||
document.getElementById('secondErrorModal').style.display = 'none';
|
||||
// Simulate retry - show the same error again after delay
|
||||
setTimeout(() => {
|
||||
showSecondError();
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([200, 100, 200]);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function closeCrashModal() {
|
||||
document.getElementById('crashModal').style.display = 'none';
|
||||
// Immediate aggressive crash
|
||||
setTimeout(() => {
|
||||
closeApp();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function sendReport() {
|
||||
// Simulate sending crash report
|
||||
alert('Отчёт о сбое отправлен разработчику');
|
||||
// Immediate crash after report
|
||||
setTimeout(() => {
|
||||
closeApp();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Function to simulate real PWA app crash
|
||||
function closeApp() {
|
||||
// Check if running as PWA (standalone mode)
|
||||
if (isInStandaloneMode()) {
|
||||
// Simulate real app crash behavior
|
||||
simulateAppCrash();
|
||||
} else {
|
||||
// If not in standalone mode, just close the browser tab
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced PWA crash simulation for iOS
|
||||
function simulateAppCrash() {
|
||||
// Method 1: iOS PWA specific minimization
|
||||
if (isInStandaloneMode() && (navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad'))) {
|
||||
try {
|
||||
// Hide app content immediately
|
||||
document.documentElement.style.cssText = 'opacity: 0 !important; transform: scale(0) !important; transition: all 0.3s ease !important;';
|
||||
|
||||
// After animation, completely hide
|
||||
setTimeout(() => {
|
||||
document.documentElement.style.display = 'none';
|
||||
document.body.style.display = 'none';
|
||||
}, 300);
|
||||
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Method 2: Service Worker termination
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'SILENT_TERMINATE'
|
||||
});
|
||||
}
|
||||
|
||||
// Method 3: Final termination attempts
|
||||
setTimeout(() => {
|
||||
try {
|
||||
window.close();
|
||||
if (window.parent !== window) {
|
||||
window.parent.close();
|
||||
}
|
||||
window.location.replace('about:blank');
|
||||
} catch (e) {
|
||||
document.documentElement.innerHTML = '';
|
||||
document.body.innerHTML = '';
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// iOS Safari specific handling
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function isInStandaloneMode() {
|
||||
return ('standalone' in window.navigator) && (window.navigator.standalone);
|
||||
}
|
||||
|
||||
// Handle app launch from home screen
|
||||
if (isInStandaloneMode()) {
|
||||
// Simulate crash notification after app opens
|
||||
setTimeout(() => {
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
new Notification('MAX', {
|
||||
body: 'Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения',
|
||||
icon: 'icons/icon-192.png',
|
||||
badge: 'icons/icon-192.png'
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Request notification permission
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
// iOS PWA install prompt
|
||||
function isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
function isInStandaloneMode() {
|
||||
return ('standalone' in window.navigator) && (window.navigator.standalone);
|
||||
}
|
||||
|
||||
// Show iOS install instructions
|
||||
if (isIOS() && !isInStandaloneMode()) {
|
||||
// Add install prompt for iOS users
|
||||
setTimeout(() => {
|
||||
const installBanner = document.createElement('div');
|
||||
installBanner.innerHTML = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(10px);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
">
|
||||
<div style="margin-bottom: 8px;">📱 Добавить MAX на главный экран</div>
|
||||
<div style="font-size: 12px; opacity: 0.8;">
|
||||
Нажмите <strong>⎙</strong> и выберите "На экран Домой"
|
||||
</div>
|
||||
<button onclick="this.parentElement.parentElement.remove()" style="
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
">×</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(installBanner);
|
||||
|
||||
// Auto hide after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (installBanner.parentElement) {
|
||||
installBanner.remove();
|
||||
}
|
||||
}, 10000);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Add CSS animation for install banner
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
76
manifest.json
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "MAX",
|
||||
"short_name": "MAX",
|
||||
"description": "Быстрое и лёгкое приложение для общения и решения повседневных задач",
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#E8E5FF",
|
||||
"theme_color": "#667eea",
|
||||
"orientation": "portrait",
|
||||
"scope": "/",
|
||||
"lang": "ru",
|
||||
"categories": ["social", "communication"],
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-120.png",
|
||||
"sizes": "120x120",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
160
sw.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
const CACHE_NAME = 'max-app-v1';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/7/75/Max_logo_2025.png'
|
||||
];
|
||||
|
||||
// Install event
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// Activate event
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Push notification event (for crash simulation)
|
||||
self.addEventListener('push', (event) => {
|
||||
const options = {
|
||||
body: 'Произошёл сбой в приложении (ошибка #4A7B-9C2E), обратитесь к разработчику приложения',
|
||||
icon: 'icons/icon-192.png',
|
||||
badge: 'icons/icon-192.png',
|
||||
vibrate: [200, 100, 200],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
action: 'restart',
|
||||
title: 'Перезапустить',
|
||||
icon: 'icon-192.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Закрыть'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('MAX - Ошибка', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click event
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'restart') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Background sync for offline functionality
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'background-sync') {
|
||||
event.waitUntil(
|
||||
// Simulate crash after sync
|
||||
new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
self.registration.showNotification('MAX - Ошибка', {
|
||||
body: 'Произошёл сбой при синхронизации (ошибка #7F3A-1D8B), обратитесь к разработчику',
|
||||
icon: 'icons/icon-192.png'
|
||||
});
|
||||
resolve();
|
||||
}, 2000);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for crash simulation messages from main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && (event.data.type === 'SIMULATE_CRASH' || event.data.type === 'FORCE_TERMINATE' || event.data.type === 'SILENT_TERMINATE')) {
|
||||
// Silent termination without errors
|
||||
try {
|
||||
// Method 1: Clean unregistration
|
||||
self.registration.unregister().then(() => {
|
||||
// Method 2: Clean cache removal
|
||||
return caches.keys();
|
||||
}).then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
}).then(() => {
|
||||
// Method 3: Silent worker termination
|
||||
self.close();
|
||||
}).catch(() => {
|
||||
// Fallback silent close
|
||||
self.close();
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
// Silent fallback
|
||||
try {
|
||||
self.close();
|
||||
} catch (e2) {
|
||||
// Do nothing - fail silently
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Silent service worker management
|
||||
setInterval(() => {
|
||||
if (Math.random() < 0.02) { // 2% chance every interval - reduced for less disruption
|
||||
try {
|
||||
// Silent memory consumption (smaller amounts to avoid browser errors)
|
||||
let memoryArray = [];
|
||||
for (let i = 0; i < 50000; i++) {
|
||||
memoryArray.push(new Array(1000).fill('data'));
|
||||
}
|
||||
|
||||
// Silent worker close
|
||||
setTimeout(() => {
|
||||
self.close();
|
||||
}, 100);
|
||||
|
||||
} catch (e) {
|
||||
// Fail silently
|
||||
}
|
||||
}
|
||||
}, 30000); // Check every 30 seconds
|
||||
|
||||
// Clean fetch handling
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
.catch(() => {
|
||||
// Silent failure - return empty response
|
||||
return new Response('', { status: 200 });
|
||||
})
|
||||
);
|
||||
});
|
||||