Compare commits

..

2 commits

Author SHA1 Message Date
Frank Denzer
48e7d76e1a add uuid lib and use it 2023-06-25 11:11:55 +02:00
Frank Denzer
96c89170a0 add logic example 2023-06-25 11:04:24 +02:00
15 changed files with 164 additions and 485 deletions

2
.gitignore vendored
View file

@ -1,4 +1,2 @@
.idea .idea
db/* db/*
backend/target/*

View file

@ -14,21 +14,3 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials, [online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. samples, guidance on mobile development, and a full API reference.
## toDo
- Einstellungen
- darkmode
- Secure Storage: https://pub.dev/packages/flutter_secure_storage
- Einkauf
- Finance
- Einkaufskorb editieren
- nur 1 Monat lang möglich
- Speichern des veränderten Warenkorbs
- redesign (statefull)
- farbliche Hervorhebungen
- ausstehende Beträge für monatliche Aufladungen
- eventuell muss sample data angepasst werden
- pre-Release:
- semanticLabels, Screenreader-Support
- Error-Management (throws)

View file

@ -1,75 +1,32 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'sample_data.dart'; import 'sample_data.dart';
/* todo:
- Flag für Ansicht/Bearbeitung
- individuelle Icons je nach Kategorie
- Pfand
- Gesamtpreis
*/
class ShowBasket extends StatelessWidget { class ShowBasket extends StatelessWidget {
final Basket basket; ShowBasket({super.key});
final bool editable;
const ShowBasket(this.basket, {this.editable = false, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: getBasket(basket),
),
const Divider(),
ListTile(
leading: const Icon(
Icons.euro,
),
title: const Text(
'Kosten:',
),
subtitle: Text(
'${basket.price}',
),
trailing: editable
? Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
backgroundColor: Colors.redAccent.shade100,
child: const Icon(Icons.remove_shopping_cart),
onPressed: () {}),
const SizedBox(
width: 10,
),
FloatingActionButton(
child: const Icon(Icons.shopping_cart_checkout),
onPressed: () {}),
],
)
: null),
],
);
}
ListView getBasket(Basket basket) {
return ListView.builder( return ListView.builder(
itemCount: basket.purchases.length, itemCount: null,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Card( if (index < SampleData().basket.purchases.length) {
child: ListTile( return Card(
leading: Text( child: ListTile(
basket.purchases.keys.elementAt(index).category.icon, leading: const Icon(Icons.abc),
style: const TextStyle(fontSize: 24), title: Text(SampleData().basket.purchases.keys.elementAt(index).name),
), //trailing: Text(),
title: Text(basket.purchases.keys.elementAt(index).name), ),
subtitle: Text( );
'${basket.purchases.values.elementAt(index)} ${basket.purchases.keys.elementAt(index).unit}'), } else {
trailing: editable return null;
? SizedBox( }
width: 100,
child: TextField(
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
]),
)
: null),
);
}, },
); );
} }

View file

@ -4,13 +4,22 @@ import 'package:intl/date_symbol_data_local.dart';
import 'basket.dart'; import 'basket.dart';
import 'sample_data.dart'; import 'sample_data.dart';
/*
todo:
- Einkauf und Settings
- Warenkorb
- Aufladungen?
- farbliche Hervorhebungen
- semanticLabels, Screenreader
- monatliche Aufladung: ausstehende Beträge
*/
class Finance extends StatelessWidget { class Finance extends StatelessWidget {
const Finance({super.key}); const Finance({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
initializeDateFormatting('de_DE'); initializeDateFormatting('de_DE');
Intl.defaultLocale = 'de_DE';
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -29,26 +38,50 @@ class Finance extends StatelessWidget {
), ),
), ),
body: ListView.builder( body: ListView.builder(
itemCount: SampleData().transactions.length, itemCount: null,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Card( if (index < SampleData().transactions.length) {
child: ListTile( return Card(
leading: getIcon(SampleData().transactions[index].type), child: ListTile(
title: leading: getIcon(SampleData().transactions[index].type),
Text(gettitle(SampleData().transactions[index].type)), title: Text(
subtitle: getSubtitle(SampleData().transactions[index]), gettitle(SampleData().transactions[index].type)),
trailing: getTrailing(context, index), subtitle: getSubtitle(index),
onTap: (SampleData().transactions[index].basket != null) trailing: SampleData().transactions[index].type ==
? () { TransaktionArt.einkauf
showBottomSheet( ? PopupMenuButton(
context: context, onSelected: (value) {},
builder: (BuildContext context) { itemBuilder: (BuildContext context) =>
return ShowBasket(SampleData() <PopupMenuEntry<String>>[
.transactions[index] const PopupMenuItem<String>(
.basket!); value: 'Option',
}); child: ListTile(
} leading: Icon(Icons.manage_history),
: null)); title: Text('Warenkorb bearbeiten')),
),
const PopupMenuItem<String>(
value: 'Option',
child: ListTile(
leading:
Icon(Icons.remove_shopping_cart),
title: Text('Einkauf stornieren')),
),
],
)
: null,
onTap: () {
if (SampleData().transactions[index].type ==
TransaktionArt.einkauf) {
showBottomSheet(
context: context,
builder: (BuildContext context) {
return ShowBasket();
});
}
}));
} else {
return null;
}
})); }));
} }
@ -61,18 +94,17 @@ class Finance extends StatelessWidget {
case TransaktionArt.korrektur: case TransaktionArt.korrektur:
return 'Korrektur'; return 'Korrektur';
case TransaktionArt.monatlBeitrag: case TransaktionArt.monatlBeitrag:
return 'Monatlicher Beitrag'; return 'monatlicher Beitrag';
default:
return 'Ein Error ist aufgetreten';
} }
} }
Text getSubtitle(Transaction transaction) { Text getSubtitle(int index) {
String text = '${transaction.amount / 100}'; String text = '${SampleData().transactions[index].amount / 100}';
text += ''; text += '';
text += DateFormat("EEEE, dd. MMMM yyyy HH:mm").format(transaction.date); text += DateFormat("EEEE, dd. MMMM yyyy HH:mm", 'de_DE')
if (transaction.description != null) { .format(SampleData().transactions[index].date);
(text += '\n${transaction.description}'); if (SampleData().transactions[index].description != null) {
(text += '\n${SampleData().transactions[index].description}');
} }
return Text(text); return Text(text);
@ -84,28 +116,10 @@ class Finance extends StatelessWidget {
return const Icon(Icons.savings); return const Icon(Icons.savings);
case TransaktionArt.einkauf: case TransaktionArt.einkauf:
return const Icon(Icons.shopping_basket); return const Icon(Icons.shopping_basket);
case TransaktionArt.korrektur:
return const Icon(Icons.priority_high);
case TransaktionArt.monatlBeitrag: case TransaktionArt.monatlBeitrag:
return const Icon(Icons.payment); return const Icon(Icons.payment);
default:
return const Icon(Icons.priority_high);
}
}
getTrailing(BuildContext context, int index) {
if (SampleData().transactions[index].type == TransaktionArt.einkauf) {
return IconButton(
icon: const Icon(Icons.manage_history),
onPressed: () {
showBottomSheet(
context: context,
builder: (BuildContext context) {
return ShowBasket(SampleData().transactions[index].basket!,
editable: true);
});
},
);
} else {
return null;
} }
} }
} }

View file

@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mitgliederladen/shopping.dart';
import 'package:mitgliederladen/settings.dart';
import 'finance.dart'; import 'finance.dart';
void main() { void main() {
@ -19,19 +17,17 @@ class MyApp extends StatelessWidget {
//darkgreen:5f7c61, mediumgreen: 66906a, lightgreen: 9cbe96, yellow: f5de64 //darkgreen:5f7c61, mediumgreen: 66906a, lightgreen: 9cbe96, yellow: f5de64
useMaterial3: true, useMaterial3: true,
brightness: Brightness.light, brightness: Brightness.light,
textTheme: Typography.englishLike2021,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
brightness: Brightness.light, brightness: Brightness.light,
seedColor: const Color(0xff5f7c61))), seedColor: const Color(0xff5f7c61))),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: Brightness.dark, brightness: Brightness.dark,
textTheme: Typography.englishLike2021,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark, brightness: Brightness.dark,
seedColor: const Color(0xff5f7c61), seedColor: const Color(0xff5f7c61),
)), )),
themeMode: ThemeMode.system, themeMode: ThemeMode.dark,
home: const MyHomePage(), home: const MyHomePage(),
); );
} }
@ -80,9 +76,21 @@ class _MyHomePageState extends State<MyHomePage> {
], ],
), ),
body: <Widget>[ body: <Widget>[
const Shopping(), Container(
const Finance(), color: Colors.red,
const Settings() alignment: Alignment.center,
child: Text('Page $test'),
),
Finance(),
ListView(children: const <Widget>[
ListTile(
leading: Icon(Icons.dark_mode),
title: Text(
'Hier könnten Einstellungen zu Darkmode mit shared_preferences und riverpod sein',
maxLines: 2,
),
)
])
][currentPageIndex], ][currentPageIndex],
); );
} }

View file

@ -1,15 +1,19 @@
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
final DateTime now = DateTime.now(); final now = DateTime.now();
enum TransaktionArt { monatlBeitrag, aufladung, einkauf, korrektur } enum TransaktionArt { monatlBeitrag, aufladung, einkauf, korrektur }
enum Unit { stueck, menge } enum Unit { stueck, menge }
class Category { enum Category {
final String name; obstUndGemuese,
final String icon; brotCerealienUndAufstriche,
const Category({required this.name, required this.icon}); getraenke,
drogerieUndHaushalt,
kochenUndBacken,
oeleSossenUndGewuerze,
suessesUndKnabbereien
} }
class Transaction { class Transaction {
@ -17,36 +21,36 @@ class Transaction {
int amount; int amount;
TransaktionArt type; TransaktionArt type;
DateTime date; DateTime date;
Basket? basket;
String? description; String? description;
Transaction( Transaction(this.type, this.amount, this.date, [this.description]);
{required this.type,
required this.amount,
required this.date,
this.description,
this.basket});
} }
class Product { class Product {
final int id = 0; final int id = 0;
final String name; final String name;
final Unit unit; //pro Kg oder Stück final Unit unit;
final double price; final double price; //pro Kilogramm oder Stück, d.h. pro unit
final double vat; final double vat;
final Category category; final Category category;
const Product(this.name, this.unit, this.price, this.vat, this.category); const Product(this.name, this.unit, this.price, this.vat, this.category);
} }
class Purchase {
Product product;
int amount;
Purchase(this.product, this.amount);
}
class Basket { class Basket {
Map<Product, int> purchases; Map<Product, int> purchases;
double price; double price;
String guid; String guid;
Basket(this.purchases, this.price) : guid = const Uuid().v4();
Basket(this.purchases, this.price): guid = const Uuid().v4();
void addItem(Product product, int quantity) { void addItem(Product product, int quantity) {
if (purchases.containsKey(product)) { if (purchases.containsKey(product)) {
purchases.update( purchases.update(product, (existingQuantity) => existingQuantity + quantity);
product, (existingQuantity) => existingQuantity + quantity);
} else { } else {
purchases[product] = quantity; purchases[product] = quantity;
} }
@ -57,167 +61,58 @@ class Basket {
} }
} }
//sample data
class SampleData { class SampleData {
static const List<Category> categories = [ final List<Transaction> transactions = [
Category(name: 'Obst und Gemüse', icon: '🍒'), Transaction(TransaktionArt.monatlBeitrag, 0, now),
Category(name: 'Kochen und Backen', icon: '🍝'), Transaction(TransaktionArt.aufladung, 2042, now),
Category(name: 'Brot, Cerealien & Aufstriche', icon: '🍞'), Transaction(
Category(name: 'Getränke und Pfand', icon: '🫖'), TransaktionArt.einkauf, -2442, now.subtract(const Duration(days: 2))),
Category(name: 'Drogerie und Haushalt', icon: '🧼'), Transaction(TransaktionArt.korrektur, 2332,
Category(name: 'Öl, Soßen und Gewürze', icon: '🫚'), now.subtract(const Duration(hours: 5)), 'Korrektur des Warenkorbs'),
Category(name: 'Süßes und Knabbereien', icon: '🍪') Transaction(TransaktionArt.monatlBeitrag, 0, now),
Transaction(TransaktionArt.aufladung, 2042, now),
Transaction(
TransaktionArt.einkauf, -2442, now.subtract(const Duration(days: 2))),
Transaction(TransaktionArt.korrektur, 2332,
now.subtract(const Duration(hours: 5)), 'Korrektur des Warenkorbs'),
Transaction(TransaktionArt.monatlBeitrag, 0, now),
Transaction(TransaktionArt.aufladung, 2042, now),
Transaction(
TransaktionArt.einkauf, -2442, now.subtract(const Duration(days: 2))),
Transaction(TransaktionArt.korrektur, 2332,
now.subtract(const Duration(hours: 5)), 'Korrektur des Warenkorbs'),
Transaction(TransaktionArt.monatlBeitrag, 0, now),
Transaction(TransaktionArt.aufladung, 2042, now),
Transaction(
TransaktionArt.einkauf, -2442, now.subtract(const Duration(days: 2))),
Transaction(TransaktionArt.korrektur, 2332,
now.subtract(const Duration(hours: 5)), 'Korrektur des Warenkorbs'),
Transaction(TransaktionArt.monatlBeitrag, 0, now),
Transaction(TransaktionArt.aufladung, 2042, now),
Transaction(
TransaktionArt.einkauf, -2442, now.subtract(const Duration(days: 2))),
Transaction(TransaktionArt.korrektur, 2332,
now.subtract(const Duration(hours: 5)), 'Korrektur des Warenkorbs')
]; ];
static List<Product> products = [ static const List<Product> products = [
Product('Apfel', Unit.stueck, 0.23, 7, categories[0]), Product('Apfel', Unit.stueck, 0.23, 7, Category.obstUndGemuese),
Product('Mehl', Unit.menge, 0.003, 19, categories[1]), Product('Mehl', Unit.menge, 0.003, 19, Category.kochenUndBacken),
Product('Brot', Unit.stueck, 1.23, 7, categories[2]), Product('Brot', Unit.stueck, 1.23, 7, Category.brotCerealienUndAufstriche),
Product('Milch', Unit.stueck, 2.23, 3, categories[3]), Product('Milch', Unit.stueck, 2.23, 3, Category.getraenke),
Product('Zahnpasta', Unit.stueck, 0.23, 7, categories[4]), Product('Zahnpasta', Unit.stueck, 0.23, 7, Category.drogerieUndHaushalt),
Product('Pfeffer', Unit.stueck, 0.23, 7, categories[5]), Product('Pfeffer', Unit.stueck, 0.23, 7, Category.oeleSossenUndGewuerze),
Product('Schokolade', Unit.menge, 0.23, 7, categories[6]), Product('Schokolade', Unit.menge, 0.23, 7, Category.suessesUndKnabbereien)
Product('Flaschenpfand', Unit.stueck, -0.15, 0, categories[3])
]; ];
//such a basket can not exist later. It is for testing purposes Basket basket = Basket({
// when scrolling through a long basket
static Basket basket = Basket({
products[0]: 20, products[0]: 20,
products[1]: 2200, products[1]: 2200,
products[2]: 2, products[2]: 2,
products[3]: 1, products[3]: 1,
products[4]: 1, products[4]: 1,
products[5]: 2, products[5]: 2,
products[6]: 222, products[6]: 202
products[0]: 20, }, 304);
products[1]: 2200,
products[2]: 2,
products[3]: 1,
products[4]: 1,
products[5]: 2,
products[6]: 222
}, 27.9);
static Basket basket2 = Basket({
products[0]: 22,
products[1]: 2241,
products[3]: 2,
products[4]: 4,
products[6]: 2,
products[7]: 5,
}, 34);
static Basket basket3 = Basket({
products[0]: 2,
products[1]: 21,
products[3]: 4,
products[4]: 1,
products[6]: 5,
}, -1);
List<Transaction> transactions = [
Transaction(
type: TransaktionArt.monatlBeitrag,
amount: 0,
date: now,
),
Transaction(
type: TransaktionArt.aufladung,
amount: 2042,
date: now,
),
Transaction(
type: TransaktionArt.einkauf,
amount: -2442,
date: now.subtract(const Duration(days: 2)),
basket: basket),
Transaction(
type: TransaktionArt.korrektur,
amount: 2332,
date: now.subtract(const Duration(hours: 5)),
description: 'Korrektur des Warenkorbs',
basket: basket3,
),
Transaction(
type: TransaktionArt.monatlBeitrag,
amount: 0,
date: now,
),
Transaction(
type: TransaktionArt.aufladung,
amount: 2042,
date: now,
),
Transaction(
type: TransaktionArt.einkauf,
amount: -2442,
date: now.subtract(const Duration(days: 2)),
basket: basket2),
Transaction(
type: TransaktionArt.korrektur,
amount: 2332,
date: now.subtract(const Duration(hours: 5)),
description: 'Korrektur des FinanzAK'),
Transaction(
type: TransaktionArt.monatlBeitrag,
amount: 0,
date: now,
),
Transaction(
type: TransaktionArt.aufladung,
amount: 2042,
date: now,
),
Transaction(
type: TransaktionArt.einkauf,
amount: -2442,
date: now.subtract(const Duration(days: 2)),
basket: basket,
),
Transaction(
type: TransaktionArt.korrektur,
amount: 2332,
date: now.subtract(const Duration(hours: 5)),
description: 'Korrektur des Warenkorbs'),
Transaction(
type: TransaktionArt.monatlBeitrag,
amount: 0,
date: now,
),
Transaction(
type: TransaktionArt.aufladung,
amount: 2042,
date: now,
),
Transaction(
type: TransaktionArt.einkauf,
amount: -2442,
date: now.subtract(const Duration(days: 2)),
basket: basket),
Transaction(
type: TransaktionArt.korrektur,
amount: 2332,
date: now.subtract(const Duration(hours: 5)),
description: 'Korrektur des Warenkorbs'),
Transaction(
type: TransaktionArt.monatlBeitrag,
amount: 0,
date: now,
),
Transaction(
type: TransaktionArt.aufladung,
amount: 2042,
date: now,
),
Transaction(
type: TransaktionArt.einkauf,
amount: -2442,
date: now.subtract(const Duration(days: 2)),
basket: basket2),
Transaction(
type: TransaktionArt.korrektur,
amount: 2332,
date: now.subtract(const Duration(hours: 5)),
description: 'Korrektur des Warenkorbs'),
];
} }

View file

@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
class Settings extends StatelessWidget {
const Settings({super.key});
@override
Widget build(BuildContext context) {
//Hier in dem Return kann der Inhalt des Setting Tabs angepasst werden
return ListView(children: const <Widget>[
ListTile(
leading: Icon(Icons.dark_mode),
title: Text(
'Hier könnten Einstellungen zu Darkmode sein',
maxLines: 2,
),
)
]);
}
}

View file

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:mitgliederladen/sample_data.dart';
class Shopping extends StatelessWidget {
const Shopping({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.shopping_cart),
onPressed: () {},
),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
itemCount: SampleData.categories.length,
itemBuilder: ((context, index) {
return Card(
child: Column(children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
SampleData.categories[index].icon,
style: const TextStyle(fontSize: 400),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
SampleData.categories[index].name,
style: Theme.of(context).textTheme.labelLarge,
),
),
]),
);
})));
}
}

View file

@ -28,6 +28,7 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer # the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`. # versions available, run `flutter pub outdated`.
dependencies: dependencies:
uuid: ^3.0.4
flutter: flutter:
sdk: flutter sdk: flutter
@ -36,7 +37,6 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
intl: ^0.18.1 intl: ^0.18.1
uuid: ^3.0.7
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,36 +1,2 @@
# Mitgliederladen # Mitgliederladen
## Development branches
backend
: Server component, using Rust/SQL, offering API (via HTTP/2?)
db_scripts
: SQL scripts
main
: frontend client as flutter app
## Tech used
### Backend
- Database connection using Rust-lang and SQLx
- RESTful (Level 2, JSON over RPC) API using Rust-lang (and probably (actix web framework)[https://actix.rs] plus (utoipa\[sic! 🍺 beer-branded punny name\])[https://github.com/juhaku/utoipa]
## Frontend
- Google Flutter using Dart.
Target platforms?
- probably Android[^1], iOS[^2].
- Debug target could be desktop for fastest deployment. Production targets: Perhaps desktop and/or web, if wanted.
[1]: via APK? Via Play Store or F-Droid? Then Registration as Play store developer (for charity or one time purchase with credit card?) needed.
[2]: Registration as Apple developer for charity needed.

View file

@ -1,7 +0,0 @@
DROP TABLE IF EXISTS mitglied;
DROP TABLE IF EXISTS transaktion_art;
DROP TABLE IF EXISTS transaktion;
DROP TABLE IF EXISTS monatlicher_beitrag;
DROP TABLE IF EXISTS mwst;
DROP TABLE IF EXISTS einheit;
DROP TABLE IF EXISTS artikel;

21
server/.gitignore vendored
View file

@ -1,21 +0,0 @@
Cargo.lock
target/
guide/build/
/gh-pages
*.so
*.out
*.pyc
*.pid
*.sock
*~
.DS_Store
# These are backup files generated by rustfmt
**/*.rs.bk
# Configuration directory generated by CLion
.idea
# Configuration directory generated by VSCode
.vscode

View file

@ -1,10 +0,0 @@
[package]
name = "server"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web ="4"
utoipa = { version = "3", features = ["actix_extras"] }

View file

@ -1,15 +0,0 @@
# installation
## windows (step 1)
choco install -y
## Linux, macos (alernate step 1)
use apt, pacman, curl, brew or whatever.
## Always (steps 2 and so on)
- `rustup-init.sh` (Windows: rustup-init.ex, e.g. %AppData%\Local\Temp\chocolatey\rustup.install\1.25.1\rustup-init.exe)
- select a good match in the dialog of this CLI installer

View file

@ -1,28 +0,0 @@
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
HttpResponse::Ok().body(req_body)
}
async fn manual_hello() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(hello)
.service(echo)
.route("/hey", web::get().to(manual_hello))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}