diff --git a/mnemo_cards_admin/web/.dockerignore b/mnemo_cards_admin/.dockerignore similarity index 100% rename from mnemo_cards_admin/web/.dockerignore rename to mnemo_cards_admin/.dockerignore diff --git a/mnemo_cards_admin/web/.gitignore b/mnemo_cards_admin/.gitignore similarity index 100% rename from mnemo_cards_admin/web/.gitignore rename to mnemo_cards_admin/.gitignore diff --git a/mnemo_cards_admin/web/@/components/ui/alert-dialog.tsx b/mnemo_cards_admin/@/components/ui/alert-dialog.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/alert-dialog.tsx rename to mnemo_cards_admin/@/components/ui/alert-dialog.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/button.tsx b/mnemo_cards_admin/@/components/ui/button.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/button.tsx rename to mnemo_cards_admin/@/components/ui/button.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/card.tsx b/mnemo_cards_admin/@/components/ui/card.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/card.tsx rename to mnemo_cards_admin/@/components/ui/card.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/dialog.tsx b/mnemo_cards_admin/@/components/ui/dialog.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/dialog.tsx rename to mnemo_cards_admin/@/components/ui/dialog.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/form.tsx b/mnemo_cards_admin/@/components/ui/form.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/form.tsx rename to mnemo_cards_admin/@/components/ui/form.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/input.tsx b/mnemo_cards_admin/@/components/ui/input.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/input.tsx rename to mnemo_cards_admin/@/components/ui/input.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/label.tsx b/mnemo_cards_admin/@/components/ui/label.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/label.tsx rename to mnemo_cards_admin/@/components/ui/label.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/select.tsx b/mnemo_cards_admin/@/components/ui/select.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/select.tsx rename to mnemo_cards_admin/@/components/ui/select.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/sonner.tsx b/mnemo_cards_admin/@/components/ui/sonner.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/sonner.tsx rename to mnemo_cards_admin/@/components/ui/sonner.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/table.tsx b/mnemo_cards_admin/@/components/ui/table.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/table.tsx rename to mnemo_cards_admin/@/components/ui/table.tsx diff --git a/mnemo_cards_admin/web/@/components/ui/textarea.tsx b/mnemo_cards_admin/@/components/ui/textarea.tsx similarity index 100% rename from mnemo_cards_admin/web/@/components/ui/textarea.tsx rename to mnemo_cards_admin/@/components/ui/textarea.tsx diff --git a/mnemo_cards_admin/Dockerfile b/mnemo_cards_admin/Dockerfile new file mode 100644 index 0000000..0b15b77 --- /dev/null +++ b/mnemo_cards_admin/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1.7 + +# --- Build stage ------------------------------------------------------------ +FROM node:20-alpine AS build +WORKDIR /app + +# Copy package files for dependency caching +COPY package*.json ./ +RUN npm ci + +# Copy source and build +COPY . . +RUN npm run build + +# Verify build output +RUN ls -la dist/ && test -f dist/index.html || (echo "ERROR: index.html not found!" && exit 1) + +# --- Runtime stage ---------------------------------------------------------- +FROM nginx:1.27-alpine AS runtime + +# Install wget for healthcheck +RUN apk add --no-cache wget + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built app +COPY --from=build /app/dist/ /usr/share/nginx/html/ + +# Verify files were copied +RUN ls -la /usr/share/nginx/html/ && test -f /usr/share/nginx/html/index.html || (echo "ERROR: index.html not copied!" && exit 1) + +EXPOSE 80 + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/ || exit 1 diff --git a/mnemo_cards_admin/web/README.md b/mnemo_cards_admin/README.md similarity index 100% rename from mnemo_cards_admin/web/README.md rename to mnemo_cards_admin/README.md diff --git a/mnemo_cards_admin/web/components.json b/mnemo_cards_admin/components.json similarity index 100% rename from mnemo_cards_admin/web/components.json rename to mnemo_cards_admin/components.json diff --git a/mnemo_cards_admin/web/eslint.config.js b/mnemo_cards_admin/eslint.config.js similarity index 100% rename from mnemo_cards_admin/web/eslint.config.js rename to mnemo_cards_admin/eslint.config.js diff --git a/mnemo_cards_admin/web/index.html b/mnemo_cards_admin/index.html similarity index 100% rename from mnemo_cards_admin/web/index.html rename to mnemo_cards_admin/index.html diff --git a/mnemo_cards_admin/nginx.conf b/mnemo_cards_admin/nginx.conf new file mode 100644 index 0000000..89b774b --- /dev/null +++ b/mnemo_cards_admin/nginx.conf @@ -0,0 +1,41 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json; + + # Security headers (enhanced for admin panel) + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1h; + add_header Cache-Control "private, must-revalidate"; + } + + # React SPA routing + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK"; + add_header Content-Type text/plain; + } + + # Security - deny access to hidden files + location ~ /\. { + deny all; + } +} diff --git a/mnemo_cards_admin/old/add_card.dart b/mnemo_cards_admin/old/add_card.dart deleted file mode 100644 index 6d318f5..0000000 --- a/mnemo_cards_admin/old/add_card.dart +++ /dev/null @@ -1,332 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:mnemo_cards/domain/router/dialogs.dart'; -import 'package:universal_io/io.dart'; - -import 'package:flutter/material.dart'; - -import 'package:file_picker/file_picker.dart'; -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../main.dart'; - -class EditCard { - GameCardDto gameCardDto; - AdminApi adminApi; - bool imageChanged = false; - - EditCard(GameCardDto? cardDto, this.adminApi) - : gameCardDto = - cardDto ?? - const GameCardDto( - id: -1, - image: null, - mnemo: null, - original: null, - translation: null, - transcription: null, - transcriptionMnemo: null, - imageBack: null, - back: null, - ); - - void _saveCard() async { - final dto = imageChanged ? gameCardDto : gameCardDto.copyWith.image(null); - final _progress = ValueNotifier<(int, int)>((0, 1)); - final _dialogCompleter = Completer(); - showInfoDialog( - '', - child: ValueListenableBuilder( - valueListenable: _progress, - builder: (c, v, _) => Text( - (v.$1 == v.$2 || v.$2 < 0) - ? 'Waiting for response...' - : 'Uploading ${v.$1}/${v.$2}...', - ), - ), - completer: _dialogCompleter, - ); - final result = await adminApi.addCard( - dto, - progressCallback: (a, b) => _progress.value = (a, b), - ); - _dialogCompleter.complete(); - if (!result) { - ScaffoldMessenger.of( - scaffoldKey.currentContext!, - ).showSnackBar(SnackBar(content: Text('ERROR'))); - } else { - ScaffoldMessenger.of( - scaffoldKey.currentContext!, - ).showSnackBar(SnackBar(content: Text('Success!!!'))); - appRouter.maybePop(); - } - } - - void _deleteCard() async { - final result = await adminApi.deleteCard(gameCardDto); - if (!result) { - ScaffoldMessenger.of( - scaffoldKey.currentContext!, - ).showSnackBar(SnackBar(content: Text('ERROR'))); - } else { - ScaffoldMessenger.of( - scaffoldKey.currentContext!, - ).showSnackBar(SnackBar(content: Text('Success!!!'))); - appRouter.maybePop(); - } - } - - void addCard(BuildContext context) { - final theme = Theme.of(context).textTheme; - - final TextEditingController originalController = TextEditingController( - text: gameCardDto.original, - ); - final TextEditingController translationController = TextEditingController( - text: gameCardDto.translation, - ); - final TextEditingController transcriptionController = TextEditingController( - text: gameCardDto.transcription, - ); - final TextEditingController transcriptionMnemoController = - TextEditingController(text: gameCardDto.transcriptionMnemo); - final TextEditingController mnemoController = TextEditingController( - text: gameCardDto.mnemo, - ); - final TextEditingController backController = TextEditingController( - text: gameCardDto.back, - ); - - showDialog( - context: context, - builder: (c) { - return Center( - child: Scaffold( - body: Material( - child: ListView( - children: [ - Header( - '${gameCardDto.id}', - hasPopButton: true, - trail: Row( - children: [ - IconButton( - onPressed: _saveCard, - icon: Icon(Icons.save), - ), - if (gameCardDto.id >= 0) - GestureDetector( - onLongPress: _deleteCard, - child: Icon(Icons.delete), - ), - ], - ), - ), - StatefulBuilder( - builder: (context, setState) { - return Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - TextField( - controller: originalController, - onChanged: (v) { - gameCardDto = gameCardDto.copyWith( - original: v, - ); - }, - decoration: InputDecoration( - hintText: 'Original (cerdo)', - ), - maxLines: 1, - style: theme.displayMedium?.copyWith( - fontSize: - theme.displayMedium!.fontSize!, - ), - ), - TextField( - controller: translationController, - onChanged: (v) { - gameCardDto = gameCardDto.copyWith( - translation: v, - ); - }, - decoration: InputDecoration( - hintText: 'Translation (свинья)', - ), - maxLines: 1, - style: theme.headlineSmall?.copyWith( - color: theme.headlineSmall!.color! - .withOpacity(0.5), - fontSize: - theme.headlineSmall!.fontSize!, - ), - ), - Row( - children: [ - Flexible( - child: TextField( - controller: transcriptionController, - onChanged: (v) { - gameCardDto = gameCardDto - .copyWith(transcription: v); - }, - decoration: InputDecoration( - hintText: 'Transcription', - ), - maxLines: 1, - style: theme.headlineSmall - ?.copyWith( - color: theme - .headlineSmall! - .color!, - fontSize: theme - .headlineSmall! - .fontSize!, - ), - ), - ), - SizedBox(width: 4.0), - Flexible( - child: TextField( - controller: - transcriptionMnemoController, - onChanged: (v) { - gameCardDto = gameCardDto - .copyWith( - transcriptionMnemo: v, - ); - }, - decoration: InputDecoration( - hintText: 'Transcription mnemo', - ), - maxLines: 1, - style: theme.headlineSmall - ?.copyWith( - color: theme - .headlineSmall! - .color!, - fontSize: theme - .headlineSmall! - .fontSize!, - ), - ), - ), - ], - ), - ], - ), - SizedBox( - width: MediaQuery.of(context).size.width - 20, - height: - MediaQuery.of(context).size.width - 20, - child: GestureDetector( - onTap: () async { - FilePickerResult? result = - await FilePicker.platform.pickFiles(); - if (result != null && - result.files.isNotEmpty) { - imageChanged = true; - final file = result.files.first; - final encoded = base64.normalize( - base64.encode( - File(file.path!).readAsBytesSync(), - ), - ); - gameCardDto = gameCardDto.copyWith( - image: encoded, - ); - } - setState(() {}); - }, - child: gameCardDto.image == null - ? Container( - alignment: Alignment.center, - color: Colors.white, - child: Icon( - Icons.image, - color: Colors.grey, - ), - ) - : Stack( - children: [ - Image( - key: ValueKey(base64), - image: MemoryImage( - base64.decode( - gameCardDto.image!, - ), - ), - gaplessPlayback: true, - fit: BoxFit.contain, - ), - Positioned( - top: 8.0, - right: 8.0, - child: IconButton( - onPressed: () { - gameCardDto = gameCardDto - .copyWith(image: null); - setState(() {}); - }, - icon: Icon(Icons.delete), - ), - ), - ], - ), - ), - ), - TextField( - controller: mnemoController, - onChanged: (v) { - gameCardDto = gameCardDto.copyWith( - mnemo: v, - ); - }, - decoration: InputDecoration( - hintText: 'Mnemo ([свинья] с сердечком)', - ), - maxLines: 1, - style: theme.headlineSmall?.copyWith( - fontSize: theme.headlineSmall!.fontSize!, - ), - ), - TextField( - controller: backController, - onChanged: (v) { - gameCardDto = gameCardDto.copyWith(back: v); - }, - decoration: InputDecoration( - hintText: 'Задняя сторона', - ), - maxLines: 1, - style: theme.headlineSmall?.copyWith( - fontSize: theme.headlineSmall!.fontSize!, - ), - ), - ], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ), - ); - }, - ); - } -} diff --git a/mnemo_cards_admin/old/add_discount_campaign.dart b/mnemo_cards_admin/old/add_discount_campaign.dart deleted file mode 100644 index 6e3d461..0000000 --- a/mnemo_cards_admin/old/add_discount_campaign.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:flutter/material.dart'; - -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../di/injector.dart'; -import '../main.dart'; -import 'date_param.dart'; -import 'enum_param.dart'; -import 'int_param.dart'; -import 'text_param.dart'; - -class AddDiscountCampaign { - late DiscountCampaignDto campaignDto; - late AdminApi adminApi; - List _campaigns = []; - - AddDiscountCampaign() { - this.adminApi = getIt.get(); - _init(null); - } - - void _init(DiscountCampaignDto? dto) { - final now = DateTime.now(); - campaignDto = dto ?? - DiscountCampaignDto( - start: now, - finish: now.add(Duration(days: 30)), - status: DiscountCampaignStatus.disabled, - name: null, - tags: [], - discounts: [], - ); - } - - Future _loadCampaigns() async { - try { - _campaigns = await adminApi.getDiscountCampaigns(); - } catch (e, s) { - log('Error', error: e, stackTrace: s); - print('Error $e'); - return false; - } - return true; - } - - void _savePromoCodeCampaign() async { - for (final d in campaignDto.discounts) { - final products = d.products.map((p) => p.toProduct()); - d.products.replaceRange(0, products.length, products); - } - - final result = await adminApi.addDiscountCampaign(campaignDto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void _deleteDiscountCampaign() async { - if (campaignDto.id == null) return; - final result = - await adminApi.deleteDiscountCampaign(campaignDto.id.toString()); - if (result != null) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void addCampaign(BuildContext context) { - showDialog( - context: context, - builder: (c) { - return FutureBuilder( - future: _loadCampaigns(), - builder: (context, snapshot) { - if (snapshot.data != true) { - return Center( - child: Material( - child: Center(child: CircularProgressIndicator()), - ), - ); - } - return Center( - child: Scaffold( - body: Material( - child: StatefulBuilder(builder: (context, setCampaign) { - return ListView( - children: [ - Header( - '${campaignDto.id}', - hasPopButton: true, - trail: Row( - children: [ - IconButton( - onPressed: _savePromoCodeCampaign, - icon: Icon(Icons.save), - ), - if (campaignDto.id != null) - IconButton( - onPressed: () async { - campaignDto = - campaignDto.copyWith(id: null); - _savePromoCodeCampaign(); - }, - icon: Icon(Icons.copy), - ), - if (campaignDto.id != null) - GestureDetector( - onLongPress: _deleteDiscountCampaign, - child: Icon(Icons.delete), - ) - ], - ), - ), - DropdownMenu( - initialSelection: null, - onSelected: (int? value) { - setCampaign(() { - _init( - _campaigns - .firstWhereOrNull((v) => v.id == value), - ); - }); - }, - dropdownMenuEntries: [ - ..._campaigns, - null, - ].map((campaign) { - return DropdownMenuEntry( - value: campaign?.id, - label: campaign == null - ? 'New' - : '${campaign.name ?? campaign.id} ${campaign.status.name}', - ); - }).toList(), - ), - StatefulBuilder(builder: (context, setState) { - return Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [ - TextParam( - 'Name', - campaignDto.name, - (v) { - campaignDto = campaignDto.copyWith( - name: v, - ); - }, - ), - DateParam( - 'Start', - campaignDto.start, - (v) { - setState(() { - campaignDto = - campaignDto.copyWith(start: v); - }); - }, - ), - DateParam( - 'Finish', - campaignDto.finish, - (v) { - setState(() { - campaignDto = - campaignDto.copyWith(finish: v); - }); - }, - ), - EnumParam( - 'Status', - DiscountCampaignStatus.values, - campaignDto.status, (v) { - setState(() { - campaignDto = - campaignDto.copyWith(status: v); - }); - }), - Text( - 'Created to create or change\n' - 'Disabled to disable\n' - 'Dont use other statuses!', - style: TextStyle(fontSize: 12), - ), - Text('Discounts:'), - ListView.builder( - shrinkWrap: true, - itemCount: - campaignDto.discounts.length + 1, - itemBuilder: (context, index) { - if (index == - campaignDto.discounts.length) { - return Container( - child: SimpleTile( - title: Text('Add'), - onTap: () async { - final discount = - await _showDiscountDialog( - context, null); - if (discount != null) { - setState(() { - final ex = campaignDto - .discounts - .firstWhereOrNull((d) => - d.id == - discount.id); - if (ex != null) { - campaignDto.discounts - .remove(ex); - } - campaignDto.discounts - .add(discount); - }); - } - }, - ), - ); - } - final item = - campaignDto.discounts[index]; - return ListTile( - title: Text( - '${item.type.name} ${item.id ?? ''}'), - onTap: () async { - final p = await _showDiscountDialog( - context, - item, - ); - if (p != null) { - setState(() { - campaignDto.discounts.add(p); - }); - } - }, - trailing: IconButton( - icon: Icon(Icons.delete), - onPressed: () { - setState(() { - campaignDto.discounts - .remove(item); - }); - }, - ), - ); - }, - ), - ], - ), - ); - }), - ], - ); - }), - ), - ), - ); - }); - }); - } - - Future _showDiscountDialog( - BuildContext context, - DiscountDto? initialDiscount, - ) async { - var prod = initialDiscount ?? DiscountDto(discountPercent: 0, products: []); - final result = await showDialog( - context: context, - builder: (c) => Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Material( - child: FutureBuilder( - future: () async { - final allPacks = await adminApi.packs(); - final allPlans = await adminApi.getSubscriptionPlans(); - return [...allPacks, ...allPlans]; - }.call(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text('Loading products...'), - ); - } - final products = snapshot.requireData; - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Adding discount'), - TextParam( - 'Id', - prod.id, - (v) { - prod = prod.copyWith(id: v); - }, - description: 'Leave empty for new', - ), - IntParam('Discount 0-100', prod.discountPercent.toInt(), - (v) { - prod = prod.copyWith( - discountPercent: v?.toDouble() ?? 0.0); - }), - StatefulBuilder(builder: (context, ss) { - return ListView.builder( - shrinkWrap: true, - itemCount: prod.products.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == prod.products.length) { - return EnumParam( - 'Add product', - products, - null, - (p) { - if (p != null) { - ss(() { - prod.products.add(p); - }); - } - }, - getName: (p) => _productName(p, products), - ); - } - final p = prod.products[index]; - return ListTile( - title: Text(_productName(p, products)), - onTap: () async { - ss(() { - prod.products.remove(p); - }); - }, - ); - }, - ); - }), - MaterialButton( - onPressed: () { - Navigator.of(context).pop(prod); - }, - child: Text('Save'), - ), - ], - ), - ); - }), - ), - ), - ), - ); - return result; - } - - String _productName(MnemoCardsProductDto? p, List products) { - if (p is CardPackPreviewDto) { - return '${p.id} ${p.title} ${p.price} pack'; - } else if (p is SubscriptionPlanAdminDto) { - return '${p.id} ${p.ui.titleLead ?? ''} ${p.ui.title ?? ''} ${p.paymentSystem.name} ${p.price} plan'; - } - final matchedProduct = products.firstWhereOrNull((m) => m.productEquals(p)); - if (matchedProduct != null) { - return _productName(matchedProduct, []); - } - return '${p?.type.name} ${p?.id}'; - } -} diff --git a/mnemo_cards_admin/old/add_package.dart b/mnemo_cards_admin/old/add_package.dart deleted file mode 100644 index f6bd2ce..0000000 --- a/mnemo_cards_admin/old/add_package.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'dart:convert'; -import 'package:universal_io/io.dart'; - -import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards/managers/repository/api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; -import '../utils/string_helper.dart'; - -import '../main.dart'; -import 'text_param.dart'; - -class AddPackage with Api { - EditCardPackDto cardPackDto; - AdminApi adminApi; - bool palleteVisible = false; - - AddPackage(EditCardPackDto? cardPackDto, this.adminApi) - : cardPackDto = cardPackDto ?? EditCardPackDto.fromJson({}); - - void _deletePack() async { - final delete = await showDialog( - context: scaffoldKey.currentContext!, - builder: (c) => Center( - child: Container( - color: Colors.white, - width: 200, - child: IconButton( - icon: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Удалить '), - Icon(Icons.delete), - ], - ), - onPressed: () { - Navigator.of(c).pop(true); - }, - ), - ), - )); - if (!delete) return; - final result = await adminApi.deletePack(cardPackDto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void _savePack() async { - final result = await adminApi.editPack(cardPackDto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void addPack(BuildContext context) { - showDialog( - context: context, - builder: (c) { - return Center( - child: Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ListView( - children: [ - Header( - '${cardPackDto.id}', - hasPopButton: true, - trail: Row( - children: [ - IconButton( - onPressed: _savePack, - icon: Icon(Icons.save), - ), - if (cardPackDto.id != null) - GestureDetector( - onLongPress: _deletePack, - child: Icon(Icons.delete), - ) - ], - ), - ), - TextParam( - 'Title', - cardPackDto.title, - (v) { - cardPackDto = cardPackDto.copyWith.title(v); - }, - ), - TextParam( - 'Subtitle', - cardPackDto.subtitle, - (v) { - cardPackDto = cardPackDto.copyWith.subtitle(v); - }, - ), - TextParam( - 'Description', - cardPackDto.description, - (v) => cardPackDto = cardPackDto.copyWith.description(v), - ), - TextParam( - 'Size', - cardPackDto.size?.toString(), - (v) { - cardPackDto = cardPackDto.copyWith.size( - int.tryParse(v), - ); - }, - ), - TextParam( - 'Google play id', - cardPackDto.googlePlayId, - (v) { - cardPackDto = cardPackDto.copyWith.googlePlayId(v); - }, - ), - TextParam( - 'Rustore id', - cardPackDto.rustoreId, - (v) { - cardPackDto = cardPackDto.copyWith.rustoreId(v); - }, - ), - TextParam( - 'Appstore id', - cardPackDto.appStoreId, - (v) { - cardPackDto = cardPackDto.copyWith.appStoreId(v); - }, - ), - TextParam( - 'Price', - cardPackDto.price, - (v) { - cardPackDto = cardPackDto.copyWith.price(v); - }, - ), - Row( - children: [ - Expanded(child: Text('Enabled ')), - StatefulBuilder(builder: (context, setState) { - return Switch( - value: cardPackDto.enabled ?? false, - onChanged: (v) { - setState(() { - cardPackDto = cardPackDto.copyWith.enabled(v); - }); - }); - }), - ], - ), - StatefulBuilder(builder: (context, setState) { - return Column( - children: [ - Row( - children: [ - Expanded(child: Text('Поменять цвет ')), - Switch( - value: palleteVisible, - onChanged: (v) => setState(() { - palleteVisible = !palleteVisible; - }), - ), - ], - ), - if (palleteVisible) - ColorPicker( - pickerAreaHeightPercent: 0.5, - paletteType: PaletteType.hsvWithHue, - pickerColor: cardPackDto.color?.asColor ?? - Colors.transparent, - onColorChanged: (color) { - String? colorString; - if (color.alpha != 0) { - colorString = '#${color.toHexString()}'; - } - cardPackDto = - cardPackDto.copyWith.color(colorString); - }, - ), - ], - ); - }), - StatefulBuilder(builder: (context, setState) { - return SizedBox( - width: MediaQuery.of(context).size.width - 20, - height: MediaQuery.of(context).size.width - 20, - child: GestureDetector( - onTap: () async { - FilePickerResult? result = - await FilePicker.platform.pickFiles(); - if (result != null && result.files.isNotEmpty) { - final file = result.files.first; - final encoded = base64.normalize( - base64.encode( - File(file.path!).readAsBytesSync(), - ), - ); - cardPackDto = cardPackDto.copyWith.cover( - encoded, - ); - } - setState(() {}); - }, - child: cardPackDto.cover == null - ? Container( - alignment: Alignment.center, - color: Colors.white, - child: Icon( - Icons.image, - color: Colors.grey, - ), - ) - : Stack( - children: [ - ImageFade( - key: ValueKey(cardPackDto.id), - image: MemoryImage( - base64.decode(cardPackDto.cover!)), - fit: BoxFit.contain, - syncDuration: Duration.zero, - ), - Positioned( - top: 8.0, - right: 8.0, - child: IconButton( - onPressed: () { - cardPackDto = cardPackDto.copyWith( - cover: null, - ); - setState(() {}); - }, - icon: Icon(Icons.delete)), - ), - ], - ), - ), - ); - }), - TextParam( - 'Preview cards', - cardPackDto.previewCards?.toString(), - (v) { - final cards = v - .decodeList() - .map((e) => e.toString()) - .toList(); - cardPackDto = cardPackDto.copyWith.previewCards(cards); - }, - ), - TextParam( - 'Order (lower is higher)', - cardPackDto.order?.toString(), - (v) { - cardPackDto = cardPackDto.copyWith.order( - int.tryParse(v), - ); - }, - ), - TextParam( - 'Cards order (ids)', - cardPackDto.cardsOrder?.join(','), - (v) { - final ids = v - .split(',') - .map((s) => int.tryParse(s.trim())) - .whereNotNull() - .toList(); - cardPackDto = cardPackDto.copyWith(cardsOrder: ids); - }, - ), - ], - ), - ), - ), - ); - }); - } -} - diff --git a/mnemo_cards_admin/old/add_plan.dart b/mnemo_cards_admin/old/add_plan.dart deleted file mode 100644 index 3ab4166..0000000 --- a/mnemo_cards_admin/old/add_plan.dart +++ /dev/null @@ -1,389 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../di/injector.dart'; -import '../main.dart'; -import 'text_param.dart'; - -class AddPlan { - List allPlans = []; - List selectedFeatures = []; - late SubscriptionPlanAdminDto planDto; - late AdminApi adminApi; - - final availableFeatures = [ - SubscriptionFeatureEnum.ads, - SubscriptionFeatureEnum.packs, - ]; - - AddPlan() { - this.adminApi = getIt.get(); - _init(null); - } - - void _init(SubscriptionPlanAdminDto? dto) { - planDto = dto ?? - const SubscriptionPlanAdminDto( - ui: SubscriptionPlanUIDto( - currency: 'руб', - ), - price: '123', - currency: 'руб', - durationDays: 0, - features: [ - SubscriptionFeatureEnum.ads, - SubscriptionFeatureEnum.packs, - ], - paymentId: null, - paymentSystem: PaymentSystem.unknown, - ); - selectedFeatures = planDto.features.toList(); - } - - Future _loadPlans() async { - final result = await adminApi.getSubscriptionPlans(); - allPlans = result; - return true; - } - - void _saveSubscriptionPlan() async { - final result = await adminApi.addSubscriptionPlan( - planDto.copyWith( - features: selectedFeatures, - ), - ); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void _deletePLan() async { - if (planDto.id == null) return; - final result = await adminApi.deleteSubscriptionPlan(planDto.id.toString()); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void addPlan(BuildContext context) { - showDialog( - context: context, - builder: (c) { - return FutureBuilder( - future: _loadPlans(), - builder: (context, snapshot) { - if (snapshot.data != true) { - return Center( - child: Material( - child: Center(child: CircularProgressIndicator()), - ), - ); - } - return Center( - child: Scaffold( - body: Material( - child: StatefulBuilder(builder: (context, setPlan) { - return ListView( - children: [ - Header( - '${planDto.id}', - hasPopButton: true, - trail: Row( - children: [ - IconButton( - onPressed: () async { - await _loadPlans(); - setPlan(() {}); - }, - icon: Icon(Icons.sync), - ), - IconButton( - onPressed: _saveSubscriptionPlan, - icon: Icon(Icons.save), - ), - if (planDto.id != null) - IconButton( - onPressed: () async { - planDto = planDto.copyWith(id: null); - _saveSubscriptionPlan(); - }, - icon: Icon(Icons.copy), - ), - if (planDto.id != null) - GestureDetector( - onLongPress: _deletePLan, - child: Icon(Icons.delete), - ) - ], - ), - ), - DropdownMenu( - initialSelection: null, - onSelected: (String? value) { - setPlan(() { - _init( - allPlans - .firstWhereOrNull((v) => v.id == value), - ); - }); - }, - dropdownMenuEntries: [ - ...allPlans, - null, - ].map((plan) { - return DropdownMenuEntry( - value: plan?.id, - label: plan == null - ? 'New' - : '${plan.ui.titleLead ?? ''} ${plan.ui.title ?? ''} ${plan.paymentSystem.name}', - ); - }).toList(), - ), - StatefulBuilder(builder: (context, setState) { - return Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - TextParam( - 'Currency', - planDto.currency, - (v) { - planDto = planDto.copyWith( - currency: v); - }, - ), - TextParam( - 'Price', - planDto.price, - (v) { - planDto = - planDto.copyWith(price: v); - }, - ), - TextParam( - 'Days', - planDto.durationDays.toString(), - (v) { - final days = int.parse(v); - planDto = planDto.copyWith( - durationDays: days, - ); - }, - ), - Text('Features:'), - ListView.builder( - shrinkWrap: true, - itemCount: - availableFeatures.length, - itemBuilder: (context, index) { - final item = - availableFeatures[index]; - return ListTile( - title: Text(item.name), - onTap: () { - setState(() { - if (selectedFeatures - .contains(item)) { - selectedFeatures - .remove(item); - } else { - selectedFeatures - .add(item); - } - }); - }, - selected: selectedFeatures - .contains(item), - selectedTileColor: Colors.blue - .withOpacity(0.5), - ); - }, - ), - Text('UI:'), - TextParam( - 'UI Currency', - planDto.ui.currency, - (v) { - final ui = planDto.ui.copyWith( - currency: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Title lead', - planDto.ui.titleLead, - (v) { - final ui = planDto.ui.copyWith( - titleLead: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Title', - planDto.ui.title, - (v) { - final ui = planDto.ui.copyWith( - title: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Price per month', - planDto.ui.pricePerMonth, - (v) { - final ui = planDto.ui.copyWith( - pricePerMonth: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Subtitle crossed', - planDto.ui.subtitleCrossed, - (v) { - final ui = planDto.ui.copyWith( - subtitleCrossed: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Subtitle', - planDto.ui.subtitle, - (v) { - final ui = planDto.ui.copyWith( - subtitle: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Tip', - planDto.ui.tip, - (v) { - final ui = planDto.ui.copyWith( - tip: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - TextParam( - 'UI Tip color', - planDto.ui.tipColor, - (v) { - final ui = planDto.ui.copyWith( - tipColor: v, - ); - planDto = - planDto.copyWith.ui(ui); - }, - ), - Padding( - padding: - const EdgeInsets.all(8.0), - child: Row( - children: [ - Text('Payment type '), - Expanded( - child: StatefulBuilder( - builder: (context, - setState) { - return DropdownButton< - PaymentSystem>( - value: planDto - .paymentSystem, - items: - PaymentSystem.values - .map( - (v) => - DropdownMenuItem( - child: Text( - v.name), - value: v, - ), - ) - .toList(), - onChanged: (v) { - setState(() { - planDto = planDto - .copyWith( - paymentSystem: v ?? - PaymentSystem - .unknown, - ); - }); - }, - ); - }), - ), - ], - ), - ), - TextParam( - 'Payment id', - planDto.paymentId, - (v) { - planDto = planDto.copyWith( - paymentId: v, - ); - }, - ), - ], - ), - ], - ), - ), - ], - ); - }), - ], - ); - }), - ), - ), - ); - }); - }); - } - - Widget uiEditor() { - return Column( - children: [], - ); - } -} diff --git a/mnemo_cards_admin/old/add_promocode_campaign.dart b/mnemo_cards_admin/old/add_promocode_campaign.dart deleted file mode 100644 index efab938..0000000 --- a/mnemo_cards_admin/old/add_promocode_campaign.dart +++ /dev/null @@ -1,399 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../di/injector.dart'; -import '../main.dart'; -import 'date_param.dart'; -import 'enum_param.dart'; -import 'int_param.dart'; -import 'text_param.dart'; - -class AddPromocodeCampaign { - late PromoCodesCampaignDto campaignDto; - late AdminApi adminApi; - List _campaigns = []; - - final availableStatuses = [ - PromoCodeCampaignStatus.created, - PromoCodeCampaignStatus.preparing, - PromoCodeCampaignStatus.ready, - PromoCodeCampaignStatus.active, - PromoCodeCampaignStatus.disabled, - ]; - - AddPromocodeCampaign() { - this.adminApi = getIt.get(); - _init(null); - } - - void _init(PromoCodesCampaignDto? dto) { - final now = DateTime.now(); - campaignDto = dto ?? - PromoCodesCampaignDto( - products: [], - activationsPerCode: 1, - activationsPerUser: 1, - generationSize: 100, - start: now, - finish: now.add(Duration(days: 60)), - status: PromoCodeCampaignStatus.disabled, - name: null, - template: r'$s$s$s$s$s$s$s$s', - tags: [], - ); - } - - Future _loadCampaigns() async { - try { - _campaigns = await adminApi.getPromoCodeCampaigns(); - } catch (e, s) { - log('Error', error: e, stackTrace: s); - print('Error $e'); - return false; - } - return true; - } - - Future _loadCampaign(String id) async { - try { - final campaign = await adminApi.getPromoCodeCampaign(id); - if (campaign != null) { - _campaigns.removeWhere((v) => v.id.toString() == id); - _campaigns.add(campaign); - campaignDto = campaign; - } - } catch (e, s) { - log('Error', error: e, stackTrace: s); - print('Error $e'); - return false; - } - return true; - } - - void _savePromoCodeCampaign() async { - final result = await adminApi.addPromoCodeCampaign(campaignDto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void _deletePromoCodeCampaign() async { - if (campaignDto.id == null) return; - final result = - await adminApi.deletePromoCodeCampaign(campaignDto.id.toString()); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void addCampaign(BuildContext context) { - showDialog( - context: context, - builder: (c) { - return FutureBuilder( - future: _loadCampaigns(), - builder: (context, snapshot) { - if (snapshot.data != true) { - return Center( - child: Material( - child: Center(child: CircularProgressIndicator()), - ), - ); - } - return Center( - child: Scaffold( - body: Material( - child: StatefulBuilder(builder: (context, setCampaign) { - final codes = campaignDto.promoCodes; - - return ListView( - children: [ - Header( - '${campaignDto.id}', - hasPopButton: true, - trail: Row( - children: [ - if (campaignDto.id != null) - IconButton( - onPressed: () async { - await _loadCampaign( - campaignDto.id.toString(), - ); - setCampaign(() {}); - }, - icon: Icon(Icons.note), - ), - IconButton( - onPressed: _savePromoCodeCampaign, - icon: Icon(Icons.save), - ), - if (campaignDto.id != null) - IconButton( - onPressed: () async { - campaignDto = - campaignDto.copyWith(id: null); - _savePromoCodeCampaign(); - }, - icon: Icon(Icons.copy), - ), - if (campaignDto.id != null) - GestureDetector( - onLongPress: _deletePromoCodeCampaign, - child: Icon(Icons.delete), - ) - ], - ), - ), - DropdownMenu( - initialSelection: null, - onSelected: (int? value) { - setCampaign(() { - _init( - _campaigns - .firstWhereOrNull((v) => v.id == value), - ); - }); - }, - dropdownMenuEntries: [ - ..._campaigns, - null, - ].map((campaign) { - return DropdownMenuEntry( - value: campaign?.id, - label: campaign == null - ? 'New' - : '${campaign.name ?? campaign.id} ${campaign.status.name}', - ); - }).toList(), - ), - StatefulBuilder(builder: (context, setState) { - return Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [ - TextParam( - 'Name', - campaignDto.name, - (v) { - campaignDto = campaignDto.copyWith( - name: v, - ); - }, - ), - TextParam( - 'Template', - campaignDto.template, - (v) { - campaignDto = campaignDto.copyWith( - template: v, - ); - }, - description: - r'$d - digit $l - letter $s - any', - ), - IntParam( - 'Generation size', - campaignDto.generationSize, - (v) { - campaignDto = campaignDto.copyWith( - generationSize: v); - }, - ), - IntParam( - 'Activations per code', - campaignDto.activationsPerCode, - (v) { - campaignDto = campaignDto.copyWith( - activationsPerCode: v, - ); - }, - ), - IntParam( - 'Activations per user', - campaignDto.activationsPerUser, - (v) { - campaignDto = campaignDto.copyWith( - activationsPerUser: v, - ); - }, - ), - DateParam( - 'Start', - campaignDto.start, - (v) { - setState(() { - campaignDto = - campaignDto.copyWith(start: v); - }); - }, - ), - DateParam( - 'Finish', - campaignDto.finish, - (v) { - setState(() { - campaignDto = - campaignDto.copyWith(finish: v); - }); - }, - ), - EnumParam( - 'Status', - PromoCodeCampaignStatus.values, - campaignDto.status, (v) { - setState(() { - campaignDto = - campaignDto.copyWith(status: v); - }); - }), - Text( - 'Created to generate promocodes\n' - 'Disabled to disable\n' - 'Dont use other statuses!', - style: TextStyle(fontSize: 12), - ), - Text('Products:'), - ListView.builder( - shrinkWrap: true, - itemCount: - campaignDto.products.length + 1, - itemBuilder: (context, index) { - if (index == - campaignDto.products.length) { - return ListTile( - title: Text('Add'), - onTap: () async { - final p = - await _showProductDialog( - context, null); - if (p != null) { - setState(() { - campaignDto.products.add(p); - }); - } - }, - ); - } - final item = - campaignDto.products[index]; - return ListTile( - title: Text( - '${item.type.name} ${item.id ?? ''}'), - onTap: () async { - final p = await _showProductDialog( - context, - item, - ); - if (p != null) { - setState(() { - campaignDto.products.add(p); - }); - } - }, - trailing: IconButton( - icon: Icon(Icons.delete), - onPressed: () { - setState(() { - campaignDto.products - .remove(item); - }); - }, - ), - ); - }, - ), - SimpleTile.text( - text: codes == null - ? 'Codes not loaded' - : 'Codes: ${codes.length == 1 ? codes.first : codes.length}', - onTap: () async { - if (codes != null) { - await Clipboard.setData( - ClipboardData( - text: codes.join('\n'), - ), - ); - } - }, - ), - ], - ), - ); - }), - ], - ); - }), - ), - ), - ); - }); - }); - } - - Future _showProductDialog( - BuildContext context, - MnemoCardsProductDto? initialProduct, - ) async { - var prod = initialProduct ?? MnemoCardsProductDto(); - final result = await showDialog( - context: context, - builder: (c) => Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Material( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextParam('Id', prod.id, (v) { - prod = prod.copyWith(id: v); - }), - EnumParam( - 'Type', - MnemoCardsProductType.values, - prod.type, - (v) { - prod = prod.copyWith(type: v); - }, - ), - MaterialButton( - onPressed: () { - Navigator.of(context).pop(prod); - }, - child: Text('Save'), - ), - ], - ), - ), - ), - ), - ); - return result; - } - - Widget uiEditor() { - return Column( - children: [], - ); - } -} diff --git a/mnemo_cards_admin/old/add_user.dart b/mnemo_cards_admin/old/add_user.dart deleted file mode 100644 index 930d9a3..0000000 --- a/mnemo_cards_admin/old/add_user.dart +++ /dev/null @@ -1,228 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:flutter/services.dart'; -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../main.dart'; - -class EditUser { - UserDto userDto; - AdminApi adminApi; - bool imageChanged = false; - - EditUser(UserDto userDto, this.adminApi) : userDto = userDto; - - void _saveUser() async { - final dto = userDto; - final result = await adminApi.editUser(dto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void _deleteCard() async { - final result = await adminApi.deleteUser(userDto); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - appRouter.maybePop(); - } - } - - void addUser(BuildContext context) { - final theme = Theme.of(context).textTheme; - - final TextEditingController nameController = - TextEditingController(text: userDto.name); - - final TextEditingController packsController = - TextEditingController(text: userDto.packs.join(',')); - - showDialog( - context: context, - builder: (c) { - return Center( - child: Scaffold( - body: Material( - child: ListView( - children: [ - Header( - '${userDto.id}', - hasPopButton: true, - trail: Row( - children: [ - IconButton( - onPressed: _saveUser, - icon: Icon(Icons.save), - ), - if (userDto.id != null && userDto.id! >= 0) - GestureDetector( - onLongPress: _deleteCard, - child: Icon(Icons.delete), - ) - ], - ), - ), - StatefulBuilder(builder: (context, setState) { - return Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [ - GestureDetector( - onTap: () { - if (userDto.id != null) { - Clipboard.setData(ClipboardData( - text: userDto.id.toString())); - } - }, - child: Text('ID: ${userDto.id}'), - ), - Text('Admin: ${userDto.admin}'), - TextField( - controller: nameController, - onChanged: (v) { - userDto = userDto.copyWith(name: v); - }, - decoration: InputDecoration(hintText: 'Name'), - maxLines: 1, - style: theme.displayMedium?.copyWith( - fontSize: theme.displayMedium!.fontSize!, - ), - ), - Row( - children: [ - Expanded(child: Text('Subscription ')), - StatefulBuilder(builder: (context, setState) { - return Switch( - value: userDto.subscription ?? false, - onChanged: (v) { - setState(() { - userDto = - userDto.copyWith.subscription(v); - }); - }); - }), - ], - ), - TextField( - controller: packsController, - onChanged: (v) { - final packs = v - .split(',') - .map((s) => - int.tryParse(s.trim())?.toString()) - .whereNotNull() - .toList(); - userDto = userDto.copyWith(packs: packs); - final packsText = packs.join(','); - if (!packsController.text - .startsWith(packsText)) { - packsController.text = packsText; - } - }, - decoration: InputDecoration(hintText: 'Packs'), - maxLines: 1, - style: theme.displayMedium?.copyWith( - fontSize: theme.displayMedium!.fontSize!, - ), - ), - ], - ), - ); - }), - _Payments(adminApi, userDto), - Text( - 'Stats: ${userDto.userDataDto?.allWordsStatistics?.total} words', - ), - Text( - 'Stats: ${userDto.userDataDto?.allTestsStatistics?.tests.length} tests', - ), - Text( - 'Last test session: ${userDto.userDataDto?.allTestsStatistics?.lastSessionToken}', - ), - ], - ), - ), - ), - ); - }); - } -} - -class _Payments extends StatelessWidget { - final AdminApi adminApi; - final UserDto userDto; - - _Payments(this.adminApi, this.userDto); - - @override - Widget build(BuildContext context) { - final sc = StreamController?>(); - final stream = sc.stream; - - return ExpansionTile( - title: Text('Транзакции'), - maintainState: true, - onExpansionChanged: (v) async { - if (v) { - sc.add(null); - final payments = - await adminApi.getUserPurchases(userDto.id.toString()); - sc.add(payments); - } - }, - children: [ - StreamBuilder?>( - stream: stream, - builder: (context, snapshot) { - final payments = snapshot.data; - if (payments == null) { - return Text('Loading...'); - } - if (payments.isEmpty) { - return Text('Нет покупок'); - } - return Column( - children: payments.map((p) { - return ListTile( - title: Text('${p.amount} ${p.currency}'), - subtitle: Text(dateString(p.date)), - trailing: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(p.status.name), - Text(p.paymentSystem.name), - Text( - p.products.map((p) => '${p.type}: ${p.id}').join('\n'), - ), - ], - ), - ); - }).toList(), - ); - }, - ), - ], - ); - } - - String dateString(DateTime date) => - '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}'; -} diff --git a/mnemo_cards_admin/old/admin_api.dart b/mnemo_cards_admin/old/admin_api.dart deleted file mode 100644 index d0c86a9..0000000 --- a/mnemo_cards_admin/old/admin_api.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:developer'; - -import 'package:dio/dio.dart'; -import 'package:mnemo_cards/di/injector.dart'; -import 'package:mnemo_cards/features/packs/packs_api.dart'; -import 'package:mnemo_cards/features/worker/worker_ext.dart'; -import 'package:mnemo_cards/managers/repository/api.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; - -class AdminApi with Api { - final Dio _dio; - - AdminApi(this._dio); - - Future addCard( - GameCardDto dto, { - ProgressCallback? progressCallback, - }) async { - final r = await _dio.post( - '$path/cards/add', - data: jsonEncode(dto.toJson()), - options: Options(sendTimeout: Duration(seconds: 30)), - onSendProgress: (a, b) { - log('Card send progress $a/$b'); - progressCallback?.call(a, b); - }, - onReceiveProgress: (a, b) { - log('Card receive progress $a/$b'); - progressCallback?.call(a, b); - }, - ); - return r.statusCode == 200; - } - - Future deleteCard(GameCardDto dto) async { - final r = await _dio.post( - '$path/cards/delete', - data: jsonEncode({'id': dto.id.toString()}), - ); - return r.statusCode == 200; - } - - Future> getCardIds() async { - final r = await _dio.get('$path/cards/ids'); - return (jsonDecode(r.data!)['ids'] as String).split(',').cast(); - } - - Future> getCards(List ids) async { - final r = await _dio.get( - '$path/cards', - queryParameters: {'ids': ids.join(',')}, - ); - return (r.data as String).decodeList(GameCardDto.fromJson); - } - - Future> getUsers(List ids) async { - final r = await _dio.get( - '$path/users', - queryParameters: {'ids': ids.join(',')}, - ); - return (jsonDecode(r.data!) as List) - .map((e) => UserDto.fromJson(e as Map)) - .toList() - .cast(); - } - - Future> getUserIds() async { - final r = await _dio.get('$path/users/ids'); - return (jsonDecode(r.data!)['ids'] as String).split(',').cast(); - } - - Future> getUserPurchases(String userId) async { - final r = await _dio.get( - '$path/user/purchases', - queryParameters: {'id': userId}, - ); - return (jsonDecode(r.data!)['payments'] as List) - .cast>() - .map(PaymentDto.fromJson) - .toList(); - } - - Future deleteUser(UserDto dto) async { - final r = await _dio.post( - '$path/users/delete', - data: jsonEncode({'id': dto.id.toString()}), - ); - return r.statusCode == 200; - } - - Future editUser(UserDto dto) async { - final r = await _dio.post( - '$path/users/add', - data: jsonEncode(dto.toJson()), - ); - return r.statusCode == 200; - } - - Future getEditPack(String id) async { - final r = await _dio.get('$path/pack/edit/$id'); - return (r.data as String).decode(EditCardPackDto.fromJson); - } - - Future deletePack(EditCardPackDto dto) async { - final r = await _dio.post( - '$path/pack/delete', - data: jsonEncode({'id': dto.id.toString()}), - ); - return r.statusCode == 200; - } - - Future editPack(EditCardPackDto dto) async { - final r = await _dio.post( - '$path/pack/edit', - data: jsonEncode(dto.toJson()), - ); - return r.statusCode == 200; - } - - Future> packs() => - getIt.get().getPacksPreviews(null); - - Future> getSubscriptionPlans() async { - final r = await _dio.get('$path/subscriptions'); - return (jsonDecode(r.data!)['plans'] as List) - .cast>() - .map(SubscriptionPlanAdminDto.fromJson) - .toList(); - } - - Future addSubscriptionPlan(SubscriptionPlanAdminDto dto) async { - final r = await _dio.post( - '$path/subscription/add', - data: jsonEncode(dto.toJson()), - ); - return r.statusCode == 200; - } - - Future deleteSubscriptionPlan(String id) async { - final r = await _dio.post('$path/subscription/delete/$id'); - return r.statusCode == 200; - } - - Future deletePromoCodeCampaign(String id) async { - final r = await _dio.post('$path/promocode/delete/$id'); - return r.statusCode == 200; - } - - Future addPromoCodeCampaign(PromoCodesCampaignDto dto) async { - final r = await _dio.post( - '$path/promocode/add', - data: dto.encode(), - ); - return r.statusCode == 200; - } - - Future> getPromoCodeCampaigns() async { - final r = await _dio.get('$path/promocode/list'); - return (jsonDecode(r.data!)['campaigns'] as List) - .cast>() - .map(PromoCodesCampaignDto.fromJson) - .toList(); - } - - Future getPromoCodeCampaign(String id) async { - final r = await _dio.get('$path/promocode/$id'); - return r.data?.isolatedDecode(PromoCodesCampaignDto.fromJson); - } - - Future addDiscountCampaign(DiscountCampaignDto dto) async { - final r = await _dio.post( - '$path/discounts/add', - data: dto.encode(), - ); - return r.statusCode == 200; - } - - Future> getDiscountCampaigns() async { - final r = await _dio.get('$path/discounts/list'); - return (jsonDecode(r.data!)['campaigns'] as List) - .cast>() - .map(DiscountCampaignDto.fromJson) - .toList(); - } - - Future deleteDiscountCampaign(String id) async { - try { - final r = await _dio.post('$path/discounts/delete/$id'); - return (jsonDecode(r.data!)['error'] as String?); - } catch (e, s) { - return e.toString(); - } - } -} diff --git a/mnemo_cards_admin/old/all_cards.dart b/mnemo_cards_admin/old/all_cards.dart deleted file mode 100644 index ceabd80..0000000 --- a/mnemo_cards_admin/old/all_cards.dart +++ /dev/null @@ -1,367 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:mnemo_cards/admin/add_card.dart'; -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards/di/injector.dart'; -import 'package:mnemo_cards/managers/repository/dio_provider.dart'; -import 'package:mnemo_cards/theme/themes.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../main.dart'; -import 'add_package.dart'; - -class AllCards extends StatefulWidget { - @override - State createState() => _AllCardsState(); - - const AllCards(); -} - -class _AllCardsState extends State { - static List ids = []; - static Set cards = {}; - static String filter = ''; - static List _packPreviews = []; - static EditCardPackDto? _currentPack; - static String? _currentId; - static List _selectedCards = []; - static Map _cache = {}; - - bool get selectingMode => _selectedCards.isNotEmpty; - - Dio get dio => getIt.get().dio; - - AdminApi get api => getIt.get(); - - Future loadCards() async { - var loadingIds = ids.toList(); - cards.clear(); - _cache.clear(); - while (loadingIds.isNotEmpty) { - try { - final ids = loadingIds.take(10).toList(); - loadingIds.removeRange(0, ids.length); - final cardsPage = await api.getCards(ids); - for (final card in cardsPage) { - final imageString = - card.image?.substring(0, min(100, card.image!.length)) ?? ''; - _cache[card.id.toString()] = - '${card.id}${card.mnemo}${card.transcription}${card.transcriptionMnemo}${card.original}${card.translation}${imageString}'; - } - cards.addAll(cardsPage); - setState(() {}); - } catch (e) { - print(e); - } - } - } - - Future loadIds() async { - ids = await api.getCardIds(); - setState(() {}); - } - - Future loadPacks() async { - _packPreviews = await api.packs(); - clearCurrentPack(); - } - - Future loadCurrentPack() async { - if (_currentId != null) { - _currentPack = await api.getEditPack(_currentId!); - setState(() {}); - } - } - - void clearCurrentPack() { - _currentId = null; - _currentPack = null; - setState(() {}); - } - - Future editPack(EditCardPackDto pack) async { - final result = await api.editPack(pack); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - loadCurrentPack(); - _selectedCards.clear(); - } - } - - void addOrEditPack() => AddPackage(_currentPack, api).addPack(context); - - List buildCards() { - final filteredCards = cards.where((dto) { - final filterOk = - filter.isEmpty || _cache[dto.id.toString()]?.contains(filter) == true; - final packFilter = - _currentPack?.addCardIds?.contains(dto.id.toString()) ?? true; - return filterOk && packFilter; - }); - return filteredCards.toList(); - } - - @override - Widget build(BuildContext context) { - final filteredCards = buildCards(); - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - ListView.builder( - itemExtentBuilder: (index, dims) => index >= 1 ? 160.h : 350, - itemBuilder: (context, index) { - if (index == 0) - return Column( - children: [ - Header( - _currentId == null - ? 'Все карточки' - : _currentPack?.title ?? - 'Пак ${_currentPack?.id}', - subtitle: filteredCards.isEmpty - ? 'Нет карточек' - : '${filteredCards.length} карточек', - hasPopButton: true, - trail: IconButton( - onPressed: () async { - setState(() {}); - await loadIds(); - await loadPacks(); - await loadCards(); - }, - icon: Icon(Icons.sync)), - ), - dropdownButton(context), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: InputDecoration(hintText: 'Поиск'), - onChanged: (v) { - if (filter != v.toLowerCase()) { - filter = v.toLowerCase(); - setState(() {}); - } - }, - ), - ), - Expanded( - child: GestureDetector( - onTap: () => EditCard(null, api).addCard(context), - child: Container( - color: Colors.green.withOpacity(0.1), - alignment: Alignment.center, - child: Icon(Icons.add, size: 30), - ), - ), - ), - ], - ); - if (filteredCards.length >= (index - 1) * 3) { - final cardIndex = (index - 1) * 3; - return Row( - children: [ - if (filteredCards.length > cardIndex) - cardWidget(filteredCards[cardIndex]), - if (filteredCards.length > cardIndex + 1) - cardWidget(filteredCards[cardIndex + 1]), - if (filteredCards.length > cardIndex + 2) - cardWidget(filteredCards[cardIndex + 2]), - ], - ); - } - return null; - }), - if (selectingMode) - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 100, - padding: EdgeInsets.all(10.0), - alignment: Alignment.center, - child: Row( - children: [ - GestureDetector( - onTap: () { - _selectedCards.clear(); - setState(() {}); - }, - child: Container( - color: Colors.blue, - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - Text('${_selectedCards.length} '), - Icon(Icons.clear) - ], - ), - ), - ), - if (_currentPack != null) - if (_selectedCards.every((c) => - _currentPack!.addCardIds?.contains(c) ?? false)) - Row( - children: [ - SizedBox(width: 8.0), - GestureDetector( - onTap: () { - final updated = - _currentPack!.copyWith.removeCardIds( - _selectedCards.toList(), - ); - editPack(updated); - }, - child: Container( - padding: EdgeInsets.all(8.0), - color: Colors.blue, - child: Text('Убрать из пака'), - ), - ), - SizedBox(width: 8.0), - GestureDetector( - onTap: () { - final updated = - _currentPack!.copyWith.previewCards( - _selectedCards.toList(), - ); - editPack(updated); - }, - child: Container( - padding: EdgeInsets.all(8.0), - color: Colors.blue, - child: Text('Как превью'), - ), - ), - ], - ) - else - MaterialButton( - onPressed: () { - final updated = _currentPack!.copyWith.addCardIds( - _selectedCards.toList(), - ); - editPack(updated); - }, - child: Container( - padding: EdgeInsets.all(8.0), - color: Colors.blue, - child: Text('Добавить в пак'), - ), - ) - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget cardWidget(GameCardDto card) { - void toggleSelect() { - final id = card.id.toString(); - if (_selectedCards.contains(id)) { - _selectedCards.remove(id); - } else { - _selectedCards.add(card.id.toString()); - } - setState(() {}); - } - - return GestureDetector( - onTap: () { - if (selectingMode) { - toggleSelect(); - } else { - EditCard(card, api).addCard(context); - } - }, - onLongPress: toggleSelect, - child: _Card(card, api, _selectedCards.contains(card.id.toString())), - ); - } - - void _onPackChanged(String? id) { - if (id == null) { - clearCurrentPack(); - } else { - _currentId = id; - loadCurrentPack(); - } - } - - Widget dropdownButton(BuildContext context) { - return Row( - children: [ - Expanded( - child: DropdownButton( - items: [..._packPreviews, null] - .map((p) => DropdownMenuItem( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: p == null - ? Text('Все паки') - : Text('${p.title} ${p.cards}'), - ), - value: p?.id, - )) - .toList(), - value: _currentId, - onChanged: (v) => _onPackChanged(v), - ), - ), - IconButton( - onPressed: addOrEditPack, - icon: _currentPack == null ? Icon(Icons.add) : Icon(Icons.edit), - ), - ], - ); - } -} - -class _Card extends StatelessWidget { - final GameCardDto cardDto; - final AdminApi api; - final bool selected; - - _Card(this.cardDto, this.api, this.selected); - - @override - Widget build(BuildContext context) { - final image = cardDto.image != null ? base64.decode(cardDto.image!) : null; - return Container( - padding: EdgeInsets.all(8.0), - width: MediaQuery.of(context).size.width / 3, - height: 160.h, - decoration: BoxDecoration( - border: Border.all(color: selected ? Colors.blue : borderGray), - color: selected ? Colors.blue.withOpacity(0.2) : null, - ), - child: Column(children: [ - Text(cardDto.translation ?? ''), - Text(cardDto.original ?? ''), - if (image != null) - Expanded( - child: Image.memory( - key: ObjectKey(cardDto.image), - gaplessPlayback: true, - image, - fit: BoxFit.scaleDown, - ), - ), - ]), - ); - } -} diff --git a/mnemo_cards_admin/old/all_users.dart b/mnemo_cards_admin/old/all_users.dart deleted file mode 100644 index bd58c30..0000000 --- a/mnemo_cards_admin/old/all_users.dart +++ /dev/null @@ -1,230 +0,0 @@ - -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:mnemo_cards/admin/add_user.dart'; -import 'package:mnemo_cards/admin/admin_api.dart'; -import 'package:mnemo_cards/di/injector.dart'; -import 'package:mnemo_cards/di/locator.dart'; -import 'package:mnemo_cards/managers/repository/dio_provider.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; - -import '../main.dart'; - -class AllUsers extends StatefulWidget { - @override - State createState() => _AllUsersState(); - - const AllUsers(); -} - -class _AllUsersState extends State { - static List ids = []; - static Set users = {}; - static String filter = ''; - static UserDto? _currentUser; - static String? _currentId; - - bool get selectingMode => false; //_selectedCards.isNotEmpty; - - Dio get dio => getIt.get().dio; - - AdminApi get api => getIt.get(); - - Future loadUsers() async { - var loadingIds = ids.toList(); - users.clear(); - while (loadingIds.isNotEmpty) { - try { - final ids = loadingIds.take(10).toList(); - loadingIds.removeRange(0, ids.length); - final usersPage = await api.getUsers(ids); - users.addAll(usersPage); - setState(() {}); - } catch (e) { - print(e); - } - } - } - - Future loadIds() async { - ids = await api.getUserIds(); - setState(() {}); - } - - Future loadCurrentUser() async { - if (_currentId != null) { - _currentUser = (await api.getUsers([_currentId!])).firstOrNull; - setState(() {}); - } - } - - void clearCurrentUser() { - _currentId = null; - _currentUser = null; - setState(() {}); - } - - Future editUser(UserDto user) async { - final result = await api.editUser(user); - if (!result) { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('ERROR'), - )); - } else { - ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( - content: Text('Success!!!'), - )); - } - } - - List buildUsersList() { - final filteredUsers = users.where((dto) { - final filterOk = filter.isEmpty || - dto.name.toString().toLowerCase().contains(filter) || - dto.id.toString().toLowerCase().contains(filter) || - dto.packs.any((p) => p.toLowerCase().contains(filter)); - return filterOk; - }); - return filteredUsers.toList(); - } - - @override - Widget build(BuildContext context) { - final filteredUsers = buildUsersList(); - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - ListView.builder(itemBuilder: (context, index) { - if (index == 0) - return Container( - height: 270, - child: Column( - children: [ - Header( - _currentId == null - ? 'Все' - : _currentUser?.name ?? 'Пак ${_currentUser?.id}', - subtitle: filteredUsers.isEmpty - ? 'Нет пользователей' - : '${filteredUsers.length} пользователя', - hasPopButton: true, - trail: IconButton( - onPressed: () async { - setState(() {}); - await loadIds(); - await loadUsers(); - }, - icon: Icon(Icons.sync)), - ), - dropdownButton(context), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: InputDecoration(hintText: 'Поиск'), - onChanged: (v) { - if (filter != v.toLowerCase()) { - filter = v.toLowerCase(); - setState(() {}); - } - }, - ), - ), - // Expanded( - // child: GestureDetector( - // onTap: () => EditUser(null, api).addCard(context), - // child: Container( - // color: Colors.green.withOpacity(0.1), - // alignment: Alignment.center, - // child: Icon(Icons.add, size: 30), - // ), - // ), - // ), - ], - ), - ); - else if (filteredUsers.length > index - 1) - return userWidget(filteredUsers[index - 1]); - else - return null; - }), - if (selectingMode) - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 100, - padding: EdgeInsets.all(10.0), - alignment: Alignment.center, - child: Row( - children: [ - GestureDetector( - onTap: () { - setState(() {}); - }, - child: Container( - color: Colors.blue, - padding: EdgeInsets.all(8.0), - child: Row( - children: [Icon(Icons.clear)], - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget userWidget(UserDto user) { - void toggleSelect() { - final id = user.id.toString(); - setState(() {}); - } - - return GestureDetector( - onTap: () { - if (selectingMode) { - toggleSelect(); - } else { - EditUser(user, api).addUser(context); - } - }, - onLongPress: toggleSelect, - child: _User(user, api, false), - ); - } - - Widget dropdownButton(BuildContext context) { - return Row( - children: [ - IconButton( - onPressed: null, - icon: _currentUser == null ? Icon(Icons.add) : Icon(Icons.edit), - ), - ], - ); - } -} - -class _User extends StatelessWidget { - final UserDto userDto; - final AdminApi api; - final bool selected; - - _User(this.userDto, this.api, this.selected); - - @override - Widget build(BuildContext context) { - final isCurrent = locator.userManager.userStateHolder.user?.id == userDto.id; - return ListTile( - title: Text('${userDto.id}${isCurrent ? ' (you)' : ''}'), - subtitle: Text(userDto.name ?? ''), - trailing: userDto.admin ? Text('admin') : null, - ); - } -} diff --git a/mnemo_cards_admin/old/date_param.dart b/mnemo_cards_admin/old/date_param.dart deleted file mode 100644 index eb3f013..0000000 --- a/mnemo_cards_admin/old/date_param.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:mnemo_cards_common/mnemo_cards_common.dart'; - -class DateParam extends StatelessWidget { - final String title; - final DateTime? initial; - final Function(DateTime? v) onChanged; - - DateParam(this.title, this.initial, this.onChanged); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: Row( - children: [ - Text(title), - SizedBox( - width: 8, - ), - Expanded( - child: Container( - padding: EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: Border.all(), - borderRadius: BorderRadius.circular(8.0), - ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final date = await showDatePicker( - context: context, - initialEntryMode: DatePickerEntryMode.calendar, - initialDate: initial ?? DateTime.now(), - firstDate: DateTime.utc(2000), - lastDate: DateTime.utc(2050), - ).then((selectedDate) { - // After selecting the date, display the time picker. - if (selectedDate != null) { - return showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ).then((selectedTime) { - // Handle the selected date and time here. - if (selectedTime != null) { - DateTime selectedDateTime = DateTime( - selectedDate.year, - selectedDate.month, - selectedDate.day, - selectedTime.hour, - selectedTime.minute, - ); - return selectedDateTime; - } - return null; - }); - } - return null; - }); - onChanged(date); - }, - child: Text( - initial?.let( - (d) => - '${d.year}.${d.month}.${d.day} ${d.hour}:${d.minute}', - ) ?? - 'No date', - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/mnemo_cards_admin/old/enum_param.dart b/mnemo_cards_admin/old/enum_param.dart deleted file mode 100644 index 6b380e1..0000000 --- a/mnemo_cards_admin/old/enum_param.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -class EnumParam extends StatelessWidget { - final String title; - final String? description; - final T initial; - final List values; - final Function(T v) onChanged; - final Function(T v)? getName; - - EnumParam( - this.title, - this.values, - this.initial, - this.onChanged, { - this.description, - this.getName, - }); - - String _defaultName(T v) => ((v is Enum) ? v.name : v.toString()); - - @override - Widget build(BuildContext context) { - T value = initial; - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title), - if (description != null) - Text( - description!, - style: TextStyle(fontSize: 12), - ) - ], - ), - SizedBox( - width: 2, - ), - Expanded( - child: StatefulBuilder(builder: (context, setState) { - return DropdownButton( - value: value, - items: values - .map( - (v) => DropdownMenuItem( - child: Text(getName?.call(v) ?? _defaultName(v)), - value: v, - ), - ) - .toList(), - onChanged: (v) => setState(() { - value = v!; - onChanged(v); - }), - ); - }), - ) - ], - ), - ); - } -} diff --git a/mnemo_cards_admin/old/int_param.dart b/mnemo_cards_admin/old/int_param.dart deleted file mode 100644 index 3c65127..0000000 --- a/mnemo_cards_admin/old/int_param.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -class IntParam extends StatelessWidget { - final String title; - final int? initial; - final Function(int? v) onChanged; - - IntParam(this.title, this.initial, this.onChanged); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Text(title+':'), - SizedBox( - width: 3, - ), - Expanded( - child: TextField( - controller: - TextEditingController(text: initial?.toString() ?? ''), - onChanged: (v) => onChanged(int.tryParse(v)), - ), - ), - ], - ), - ); - } -} diff --git a/mnemo_cards_admin/old/text_param.dart b/mnemo_cards_admin/old/text_param.dart deleted file mode 100644 index b6e8ffe..0000000 --- a/mnemo_cards_admin/old/text_param.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -class TextParam extends StatelessWidget { - final String title; - final String? description; - final String? initial; - final Function(String v) onChanged; - - TextParam(this.title, this.initial, this.onChanged, {this.description}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title), - if (description != null) - Text( - description!, - style: TextStyle(fontSize: 12), - ) - ], - ), - SizedBox( - width: 2, - ), - Expanded( - child: TextField( - controller: TextEditingController(text: initial), - onChanged: onChanged, - ), - ), - ], - ), - ); - } -} diff --git a/mnemo_cards_admin/web/package-lock.json b/mnemo_cards_admin/package-lock.json similarity index 100% rename from mnemo_cards_admin/web/package-lock.json rename to mnemo_cards_admin/package-lock.json diff --git a/mnemo_cards_admin/web/package.json b/mnemo_cards_admin/package.json similarity index 100% rename from mnemo_cards_admin/web/package.json rename to mnemo_cards_admin/package.json diff --git a/mnemo_cards_admin/web/postcss.config.cjs b/mnemo_cards_admin/postcss.config.cjs similarity index 100% rename from mnemo_cards_admin/web/postcss.config.cjs rename to mnemo_cards_admin/postcss.config.cjs diff --git a/mnemo_cards_admin/web/public/vite.svg b/mnemo_cards_admin/public/vite.svg similarity index 100% rename from mnemo_cards_admin/web/public/vite.svg rename to mnemo_cards_admin/public/vite.svg diff --git a/mnemo_cards_admin/web/src/App.css b/mnemo_cards_admin/src/App.css similarity index 100% rename from mnemo_cards_admin/web/src/App.css rename to mnemo_cards_admin/src/App.css diff --git a/mnemo_cards_admin/web/src/App.tsx b/mnemo_cards_admin/src/App.tsx similarity index 100% rename from mnemo_cards_admin/web/src/App.tsx rename to mnemo_cards_admin/src/App.tsx diff --git a/mnemo_cards_admin/web/src/api/analytics.ts b/mnemo_cards_admin/src/api/analytics.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/analytics.ts rename to mnemo_cards_admin/src/api/analytics.ts diff --git a/mnemo_cards_admin/web/src/api/auth.test.ts b/mnemo_cards_admin/src/api/auth.test.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/auth.test.ts rename to mnemo_cards_admin/src/api/auth.test.ts diff --git a/mnemo_cards_admin/web/src/api/auth.ts b/mnemo_cards_admin/src/api/auth.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/auth.ts rename to mnemo_cards_admin/src/api/auth.ts diff --git a/mnemo_cards_admin/web/src/api/cards.ts b/mnemo_cards_admin/src/api/cards.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/cards.ts rename to mnemo_cards_admin/src/api/cards.ts diff --git a/mnemo_cards_admin/web/src/api/client.test.ts b/mnemo_cards_admin/src/api/client.test.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/client.test.ts rename to mnemo_cards_admin/src/api/client.test.ts diff --git a/mnemo_cards_admin/web/src/api/client.ts b/mnemo_cards_admin/src/api/client.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/client.ts rename to mnemo_cards_admin/src/api/client.ts diff --git a/mnemo_cards_admin/web/src/api/packs.ts b/mnemo_cards_admin/src/api/packs.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/packs.ts rename to mnemo_cards_admin/src/api/packs.ts diff --git a/mnemo_cards_admin/web/src/api/users.ts b/mnemo_cards_admin/src/api/users.ts similarity index 100% rename from mnemo_cards_admin/web/src/api/users.ts rename to mnemo_cards_admin/src/api/users.ts diff --git a/mnemo_cards_admin/web/src/assets/react.svg b/mnemo_cards_admin/src/assets/react.svg similarity index 100% rename from mnemo_cards_admin/web/src/assets/react.svg rename to mnemo_cards_admin/src/assets/react.svg diff --git a/mnemo_cards_admin/web/src/components/layout/Layout.tsx b/mnemo_cards_admin/src/components/layout/Layout.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/layout/Layout.tsx rename to mnemo_cards_admin/src/components/layout/Layout.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/alert-dialog.tsx b/mnemo_cards_admin/src/components/ui/alert-dialog.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/alert-dialog.tsx rename to mnemo_cards_admin/src/components/ui/alert-dialog.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/badge.tsx b/mnemo_cards_admin/src/components/ui/badge.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/badge.tsx rename to mnemo_cards_admin/src/components/ui/badge.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/button.tsx b/mnemo_cards_admin/src/components/ui/button.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/button.tsx rename to mnemo_cards_admin/src/components/ui/button.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/card.tsx b/mnemo_cards_admin/src/components/ui/card.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/card.tsx rename to mnemo_cards_admin/src/components/ui/card.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/checkbox.tsx b/mnemo_cards_admin/src/components/ui/checkbox.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/checkbox.tsx rename to mnemo_cards_admin/src/components/ui/checkbox.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/dialog.tsx b/mnemo_cards_admin/src/components/ui/dialog.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/dialog.tsx rename to mnemo_cards_admin/src/components/ui/dialog.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/input.tsx b/mnemo_cards_admin/src/components/ui/input.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/input.tsx rename to mnemo_cards_admin/src/components/ui/input.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/label.tsx b/mnemo_cards_admin/src/components/ui/label.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/label.tsx rename to mnemo_cards_admin/src/components/ui/label.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/sonner.tsx b/mnemo_cards_admin/src/components/ui/sonner.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/sonner.tsx rename to mnemo_cards_admin/src/components/ui/sonner.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/table.tsx b/mnemo_cards_admin/src/components/ui/table.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/table.tsx rename to mnemo_cards_admin/src/components/ui/table.tsx diff --git a/mnemo_cards_admin/web/src/components/ui/textarea.tsx b/mnemo_cards_admin/src/components/ui/textarea.tsx similarity index 100% rename from mnemo_cards_admin/web/src/components/ui/textarea.tsx rename to mnemo_cards_admin/src/components/ui/textarea.tsx diff --git a/mnemo_cards_admin/web/src/index.css b/mnemo_cards_admin/src/index.css similarity index 100% rename from mnemo_cards_admin/web/src/index.css rename to mnemo_cards_admin/src/index.css diff --git a/mnemo_cards_admin/web/src/lib/utils.ts b/mnemo_cards_admin/src/lib/utils.ts similarity index 100% rename from mnemo_cards_admin/web/src/lib/utils.ts rename to mnemo_cards_admin/src/lib/utils.ts diff --git a/mnemo_cards_admin/web/src/main.tsx b/mnemo_cards_admin/src/main.tsx similarity index 100% rename from mnemo_cards_admin/web/src/main.tsx rename to mnemo_cards_admin/src/main.tsx diff --git a/mnemo_cards_admin/web/src/pages/CardsPage.tsx b/mnemo_cards_admin/src/pages/CardsPage.tsx similarity index 100% rename from mnemo_cards_admin/web/src/pages/CardsPage.tsx rename to mnemo_cards_admin/src/pages/CardsPage.tsx diff --git a/mnemo_cards_admin/web/src/pages/DashboardPage.tsx b/mnemo_cards_admin/src/pages/DashboardPage.tsx similarity index 100% rename from mnemo_cards_admin/web/src/pages/DashboardPage.tsx rename to mnemo_cards_admin/src/pages/DashboardPage.tsx diff --git a/mnemo_cards_admin/web/src/pages/LoginPage.tsx b/mnemo_cards_admin/src/pages/LoginPage.tsx similarity index 100% rename from mnemo_cards_admin/web/src/pages/LoginPage.tsx rename to mnemo_cards_admin/src/pages/LoginPage.tsx diff --git a/mnemo_cards_admin/web/src/pages/PacksPage.tsx b/mnemo_cards_admin/src/pages/PacksPage.tsx similarity index 100% rename from mnemo_cards_admin/web/src/pages/PacksPage.tsx rename to mnemo_cards_admin/src/pages/PacksPage.tsx diff --git a/mnemo_cards_admin/web/src/pages/UsersPage.tsx b/mnemo_cards_admin/src/pages/UsersPage.tsx similarity index 100% rename from mnemo_cards_admin/web/src/pages/UsersPage.tsx rename to mnemo_cards_admin/src/pages/UsersPage.tsx diff --git a/mnemo_cards_admin/web/src/stores/authStore.ts b/mnemo_cards_admin/src/stores/authStore.ts similarity index 100% rename from mnemo_cards_admin/web/src/stores/authStore.ts rename to mnemo_cards_admin/src/stores/authStore.ts diff --git a/mnemo_cards_admin/web/src/test/setup.ts b/mnemo_cards_admin/src/test/setup.ts similarity index 100% rename from mnemo_cards_admin/web/src/test/setup.ts rename to mnemo_cards_admin/src/test/setup.ts diff --git a/mnemo_cards_admin/web/src/types/models.ts b/mnemo_cards_admin/src/types/models.ts similarity index 100% rename from mnemo_cards_admin/web/src/types/models.ts rename to mnemo_cards_admin/src/types/models.ts diff --git a/mnemo_cards_admin/web/tailwind.config.js b/mnemo_cards_admin/tailwind.config.js similarity index 100% rename from mnemo_cards_admin/web/tailwind.config.js rename to mnemo_cards_admin/tailwind.config.js diff --git a/mnemo_cards_admin/web/tsconfig.app.json b/mnemo_cards_admin/tsconfig.app.json similarity index 100% rename from mnemo_cards_admin/web/tsconfig.app.json rename to mnemo_cards_admin/tsconfig.app.json diff --git a/mnemo_cards_admin/web/tsconfig.json b/mnemo_cards_admin/tsconfig.json similarity index 100% rename from mnemo_cards_admin/web/tsconfig.json rename to mnemo_cards_admin/tsconfig.json diff --git a/mnemo_cards_admin/web/tsconfig.node.json b/mnemo_cards_admin/tsconfig.node.json similarity index 100% rename from mnemo_cards_admin/web/tsconfig.node.json rename to mnemo_cards_admin/tsconfig.node.json diff --git a/mnemo_cards_admin/web/vite.config.ts b/mnemo_cards_admin/vite.config.ts similarity index 100% rename from mnemo_cards_admin/web/vite.config.ts rename to mnemo_cards_admin/vite.config.ts diff --git a/mnemo_cards_admin/web/Dockerfile b/mnemo_cards_admin/web/Dockerfile deleted file mode 100644 index 90f9ea7..0000000 --- a/mnemo_cards_admin/web/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -# syntax=docker/dockerfile:1.7 - -# --- Build stage ------------------------------------------------------------ -FROM node:20-alpine AS build -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY . . -RUN npm run build - -# --- Runtime stage ---------------------------------------------------------- -FROM nginx:1.27-alpine AS runtime -WORKDIR /usr/share/nginx/html - -# Replace default config with SPA-aware config -COPY nginx.conf /etc/nginx/conf.d/default.conf - -COPY --from=build /app/dist/ ./ - -EXPOSE 80 - -HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ - CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1 diff --git a/mnemo_cards_admin/web/nginx.conf b/mnemo_cards_admin/web/nginx.conf deleted file mode 100644 index ef426cb..0000000 --- a/mnemo_cards_admin/web/nginx.conf +++ /dev/null @@ -1,70 +0,0 @@ -# HTTP server (SSL will be configured later) -# server { -# listen 80; -# server_name admin.mnemo-cards.online; -# -# # Redirect HTTP to HTTPS -# return 301 https://$server_name$request_uri; -# } - -server { - listen 80; - server_name admin.mnemo-cards.online; - - # Note: SSL certificate not yet configured - using HTTP for now - # TODO: Set up Let's Encrypt certificate for HTTPS - # ssl_certificate /etc/letsencrypt/live/admin.mnemo-cards.online/fullchain.pem; - # ssl_certificate_key /etc/letsencrypt/live/admin.mnemo-cards.online/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - - # Security headers (enhanced for admin panel) - add_header X-Frame-Options "DENY" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.mnemo-cards.online;" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - # Root directory for admin panel - root /var/www/mnemo_cards_admin; - index index.html; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/json; - - # Main location block - location / { - try_files $uri $uri/ /index.html; - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1h; # Shorter cache for admin panel - add_header Cache-Control "private, must-revalidate"; - } - } - - # Security - deny access to hidden files and sensitive paths - location ~ /\. { - deny all; - } - - location ~ ^/admin/ { - deny all; # Prevent access to any /admin/ paths that might be exposed - } - - # Rate limiting for admin panel - limit_req zone=admin burst=10 nodelay; - limit_req_status 429; - - # Log admin access - access_log /var/log/nginx/admin_access.log; - error_log /var/log/nginx/admin_error.log; -} - -# Rate limiting zone for admin panel -limit_req_zone $binary_remote_addr zone=admin:10m rate=5r/m; diff --git a/mnemo_cards_telegram_bot/.dockerignore b/mnemo_cards_telegram_bot/.dockerignore new file mode 100644 index 0000000..3a3ffc8 --- /dev/null +++ b/mnemo_cards_telegram_bot/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitmodules +.github +.idea +.vscode +.cursor +build +.dart_tool +pubspec.lock +*.log +*.tmp +**/.DS_Store +test/ +*.exe +*.dylib +*.so +zips/ +backups/ +apks/ diff --git a/mnemo_cards_telegram_bot/Dockerfile b/mnemo_cards_telegram_bot/Dockerfile new file mode 100644 index 0000000..4649065 --- /dev/null +++ b/mnemo_cards_telegram_bot/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1.7 + +# --- Build stage ------------------------------------------------------------ +# Note: This Dockerfile should be built from the parent directory: +# docker build -f mnemo_cards_telegram_bot/Dockerfile -t mnemo_cards_telegram_bot . + +FROM dart:stable-sdk AS build +WORKDIR /app + +# Copy all local dependencies +COPY mnemo_cards_common /app/mnemo_cards_common +COPY mnemo_cards_common_backend /app/mnemo_cards_common_backend +COPY mnemo_cards_telegram_bot /app/mnemo_cards_telegram_bot + +# Get dependencies (order matters due to path dependencies) +WORKDIR /app/mnemo_cards_common +RUN dart pub get + +WORKDIR /app/mnemo_cards_common_backend +RUN dart pub get + +WORKDIR /app/mnemo_cards_telegram_bot +RUN dart pub get + +# Compile to native executable +RUN dart compile exe bin/main.dart -o /app/bot + +# --- Runtime stage ---------------------------------------------------------- +FROM debian:bookworm-slim AS runtime +WORKDIR /app + +# Install runtime dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + zip \ + && rm -rf /var/lib/apt/lists/* + +# Copy bot binary +COPY --from=build /app/bot /app/bot + +# Copy required files +COPY --from=build /app/mnemo_cards_telegram_bot/admins /app/admins + +# Create required directories +RUN mkdir -p /app/backups /app/zips /app/apks && \ + chmod -R 755 /app/backups /app/zips /app/apks + +# Environment variables +ENV BACKEND_URL=https://api.mnemo-cards.online \ + BOT_SHARE_DAILY_LIMIT=1 + +# No EXPOSE needed - bot uses outbound connections only (polling) + +# Healthcheck - check if process is running +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ + CMD pgrep -f "/app/bot" > /dev/null || exit 1 + +# Start bot +CMD ["/app/bot"]