Skip to content

Commit 1ef6024

Browse files
committed
2 parents 59d2764 + 0d40e08 commit 1ef6024

File tree

4 files changed

+106
-130
lines changed

4 files changed

+106
-130
lines changed

backend/bin/server.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,5 @@ void main(List<String> args) async {
154154
final server = await io.serve(handler, '0.0.0.0', port);
155155
print('Server running on port \${server.port}');
156156
}
157+
158+
// couldn't insert new routes

backend/bin/worker.dart

Lines changed: 28 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,38 @@
1-
import 'dart:convert';
21
import 'dart:io';
3-
import 'package:dotenv/dotenv.dart' as dotenv;
4-
import '../lib/db.dart';
5-
import '../lib/connectors/scraper.dart';
2+
import 'dart:convert';
3+
import 'package:postgres/postgres.dart';
64

7-
Future<void> processJob(DB db, Map row) async {
8-
final id = row['id'] as int;
9-
final payload = jsonDecode(row['payload'] as String) as Map<String, dynamic>;
10-
final type = row['type'] as String;
11-
print('Processing job \$id type=\$type payload=\$payload');
12-
try {
13-
if (type == 'scrape_product') {
14-
final url = payload['url'] as String;
15-
final scraped = await scrapeProductFromUrl(url);
16-
if (scraped == null) throw Exception('scrape returned null');
17-
final conn = db.conn;
18-
await conn.transaction((ctx) async {
19-
await ctx.query('''
20-
INSERT INTO products (external_id, title, price, image, source_url, updated_at)
21-
VALUES (@external_id, @title, @price, @image, @source_url, now())
22-
ON CONFLICT (source_url) DO UPDATE SET
23-
title = EXCLUDED.title,
24-
price = EXCLUDED.price,
25-
image = EXCLUDED.image,
26-
updated_at = now();
27-
''', substitutionValues: {
28-
'external_id': scraped['external_id'],
29-
'title': scraped['title'],
30-
'price': scraped['price'],
31-
'image': scraped['image'],
32-
'source_url': scraped['source_url']
33-
});
34-
await ctx.query('UPDATE jobs SET status = @status, updated_at = now() WHERE id = @id', substitutionValues: {
35-
'status': 'done',
36-
'id': id
37-
});
38-
});
39-
} else {
40-
throw Exception('unknown job type');
41-
}
42-
} catch (e) {
43-
print('Job \$id failed: \$e');
44-
final attempts = (row['attempts'] as int) + 1;
45-
final conn = db.conn;
46-
final nextRun = DateTime.now().add(Duration(seconds: attempts * 10));
47-
await conn.query('UPDATE jobs SET attempts = @attempts, last_error = @err, status = @status, run_at = @run_at, updated_at = now() WHERE id = @id', substitutionValues: {
48-
'attempts': attempts,
49-
'err': e.toString(),
50-
'status': 'pending',
51-
'run_at': nextRun.toUtc().toIso8601String(),
52-
'id': id
53-
});
54-
}
5+
Future<PostgreSQLConnection> connectDb() async {
6+
final dbUrl = Platform.environment['DATABASE_URL'] ?? 'postgres://mymodus:mymodus_pass@localhost:5432/mymodus_db';
7+
final uri = Uri.parse(dbUrl);
8+
final conn = PostgreSQLConnection(uri.host, uri.port, uri.path.replaceFirst('/', ''),
9+
username: uri.userInfo.split(':').first,
10+
password: uri.userInfo.split(':').length > 1 ? uri.userInfo.split(':')[1] : null);
11+
await conn.open();
12+
return conn;
5513
}
5614

57-
Future<void> pollLoop(DB db) async {
58-
while (true) {
15+
void main(List<String> args) async {
16+
print('Worker started');
17+
final conn = await connectDb();
18+
19+
// Simple example job: ensure marketplaces exist
20+
final marketplaces = [
21+
{'code': 'ozon', 'name': 'Ozon'},
22+
{'code': 'wb', 'name': 'Wildberries'},
23+
{'code': 'lamoda', 'name': 'Lamoda'}
24+
];
25+
for (final m in marketplaces) {
5926
try {
60-
final rows = await db.conn.mappedResultsQuery('''
61-
SELECT id, type, payload, attempts FROM jobs
62-
WHERE status = 'pending' AND run_at <= now()
63-
ORDER BY created_at ASC
64-
LIMIT 1
65-
''');
66-
if (rows.isEmpty) {
67-
await Future.delayed(Duration(seconds: 2));
68-
continue;
69-
}
70-
final row = rows.first.values.first;
71-
await db.conn.query('UPDATE jobs SET status = @status, updated_at = now() WHERE id = @id', substitutionValues: {
72-
'status': 'processing',
73-
'id': row['id']
74-
});
75-
await processJob(db, row);
27+
await conn.query('INSERT INTO marketplaces (code, name) VALUES (@code, @name) ON CONFLICT (code) DO NOTHING', substitutionValues: m);
7628
} catch (e) {
77-
print('Worker loop error: \$e');
78-
await Future.delayed(Duration(seconds: 5));
29+
print('Marketplace insert error: \$e');
7930
}
8031
}
81-
}
8232

83-
void main(List<String> args) async {
84-
dotenv.load();
85-
final db = await DB.connect();
86-
print('Worker connected to DB, starting poll loop...');
87-
await pollLoop(db);
33+
// Placeholder: here would be scheduler loop fetching from queue and running scrapers
34+
while (true) {
35+
print('Worker heartbeat: ' + DateTime.now().toIso8601String());
36+
await Future.delayed(Duration(seconds: 30));
37+
}
8838
}

frontend/lib/main.dart

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,48 @@
11
import 'package:flutter/material.dart';
2-
import 'app.dart';
2+
import 'services/api.dart';
3+
import 'screens/login_screen.dart';
4+
import 'screens/register_screen.dart';
5+
import 'screens/feed_screen.dart';
36

4-
void main() async {
5-
WidgetsFlutterBinding.ensureInitialized();
6-
runApp(MyModusApp());
7+
void main() {
8+
runApp(const MyApp());
9+
}
10+
11+
class MyApp extends StatelessWidget {
12+
const MyApp({super.key});
13+
@override
14+
Widget build(BuildContext context) {
15+
final api = ApiService('http://localhost:8080');
16+
return MaterialApp(
17+
title: 'MyModus',
18+
home: Home(api: api),
19+
);
20+
}
21+
}
22+
23+
class Home extends StatelessWidget {
24+
final ApiService api;
25+
const Home({super.key, required this.api});
26+
27+
@override
28+
Widget build(BuildContext context) {
29+
return Scaffold(
30+
appBar: AppBar(title: const Text('MyModus')),
31+
body: Center(
32+
child: Column(mainAxisSize: MainAxisSize.min, children: [
33+
ElevatedButton(
34+
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => FeedScreen(api: api))),
35+
child: const Text('Feed')),
36+
const SizedBox(height: 8),
37+
ElevatedButton(
38+
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LoginScreen(api: api))),
39+
child: const Text('Login')),
40+
const SizedBox(height: 8),
41+
ElevatedButton(
42+
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => RegisterScreen(api: api))),
43+
child: const Text('Register')),
44+
]),
45+
),
46+
);
47+
}
748
}

frontend/lib/screens/feed_screen.dart

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,55 @@
11
import 'package:flutter/material.dart';
22
import '../services/api.dart';
3-
import '../components/product_card.dart';
4-
import '../components/story_widget.dart';
5-
import 'product_detail.dart';
6-
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
3+
import 'item_screen.dart';
74

85
class FeedScreen extends StatefulWidget {
6+
final ApiService api;
7+
const FeedScreen({super.key, required this.api});
8+
99
@override
10-
_FeedScreenState createState() => _FeedScreenState();
10+
State<FeedScreen> createState() => _FeedScreenState();
1111
}
1212

1313
class _FeedScreenState extends State<FeedScreen> {
14-
List<dynamic> items = [];
14+
List posts = [];
1515
bool loading = true;
1616

1717
@override
1818
void initState() {
1919
super.initState();
20-
load();
20+
_load();
2121
}
2222

23-
void load() async {
24-
try {
25-
final res = await fetchProducts();
26-
setState(() { items = res; loading = false; });
27-
} catch (e) {
28-
setState(() { items = []; loading = false; });
23+
void _load() async {
24+
setState(() { loading = true; });
25+
final res = await widget.api.getFeed();
26+
if (res.containsKey('posts')) {
27+
setState(() { posts = res['posts']; loading = false; });
28+
} else {
29+
setState(() { loading = false; });
2930
}
3031
}
3132

3233
@override
3334
Widget build(BuildContext context) {
34-
final demoStories = [
35-
StoryItem(id:'s1', image:'https://picsum.photos/seed/1/400/800', title:'New in'),
36-
StoryItem(id:'s2', image:'https://picsum.photos/seed/2/400/800', title:'Sale'),
37-
StoryItem(id:'s3', image:'https://picsum.photos/seed/3/400/800', title:'Trending'),
38-
];
3935
return Scaffold(
40-
appBar: AppBar(title: Text('MyModus')),
41-
body: loading ? Center(child:CircularProgressIndicator()) : RefreshIndicator(
42-
onRefresh: () async { load(); },
43-
child: CustomScrollView(
44-
slivers: [
45-
SliverToBoxAdapter(child: StoriesReel(items: demoStories)),
46-
SliverPadding(
47-
padding: EdgeInsets.symmetric(horizontal:12, vertical:8),
48-
sliver: SliverMasonryGrid.count(
49-
crossAxisCount: 2,
50-
mainAxisSpacing: 8,
51-
crossAxisSpacing: 8,
52-
childCount: items.length,
53-
itemBuilder: (ctx, i) {
54-
final p = items[i];
55-
return GestureDetector(
56-
onDoubleTap: (){}, // handled in card
57-
child: ProductCard(product: p, onTap: () {
58-
Navigator.push(context, MaterialPageRoute(builder: (_) => ProductDetail(product: p)));
59-
}),
60-
);
61-
},
62-
),
63-
)
64-
],
65-
),
66-
),
67-
floatingActionButton: FloatingActionButton(
68-
onPressed: () => Navigator.pushNamed(context, '/create_post'),
69-
child: Icon(Icons.add),
36+
appBar: AppBar(title: const Text('Feed')),
37+
body: loading ? const Center(child: CircularProgressIndicator()) :
38+
ListView.builder(
39+
itemCount: posts.length,
40+
itemBuilder: (context, idx) {
41+
final p = posts[idx];
42+
final item = p['item'];
43+
return Card(
44+
margin: const EdgeInsets.all(8),
45+
child: ListTile(
46+
leading: item['image'] != null ? Image.network(item['image'], width: 56, height: 56, fit: BoxFit.cover) : null,
47+
title: Text(item['title'] ?? ''),
48+
subtitle: Text('${item['price'] ?? ''} ${item['currency'] ?? ''}'),
49+
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ItemScreen(api: widget.api, itemId: item['id']))),
50+
),
51+
);
52+
},
7053
),
7154
);
7255
}

0 commit comments

Comments
 (0)