Skip to content

Commit 63fbca5

Browse files
committed
feat: add fake loading effect and make course table grid stateful
1 parent fcb1550 commit 63fbca5

File tree

1 file changed

+96
-7
lines changed

1 file changed

+96
-7
lines changed

lib/screens/main/course_table/course_table_grid.dart

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ List<Period> _periods = [
3434
Period.dPeriod,
3535
];
3636

37-
class CourseTableGrid extends StatelessWidget {
37+
class CourseTableGrid extends StatefulWidget {
3838
const CourseTableGrid({
3939
super.key,
4040
required this.couseTableSummary,
@@ -50,12 +50,28 @@ class CourseTableGrid extends StatelessWidget {
5050
/// Initial visible height of the grid viewport (before user scrolls).
5151
final double? viewportHeight;
5252

53+
@override
54+
State<CourseTableGrid> createState() => _CourseTableGridState();
55+
}
56+
57+
class _CourseTableGridState extends State<CourseTableGrid> {
58+
bool _loading = true;
59+
5360
final double _tableHeaderHeight = 25;
5461
final double _stubWidth = 20;
5562

5663
// TODO: dynamic row height based on viewport height
5764
final double _periodRowHeight = 64;
5865

66+
@override
67+
void initState() {
68+
super.initState();
69+
final random = Random();
70+
Future.delayed(Duration(milliseconds: random.nextInt(1000)), () {
71+
if (mounted) setState(() => _loading = false);
72+
});
73+
}
74+
5975
@override
6076
Widget build(BuildContext context) {
6177
return CustomScrollView(
@@ -74,7 +90,10 @@ class CourseTableGrid extends StatelessWidget {
7490
),
7591
SliverToBoxAdapter(
7692
child: Stack(
77-
children: [_buildPeriodRows(), ..._buildCourseBlocks()],
93+
children: [
94+
_buildPeriodRows(),
95+
...(_loading ? _bulidSkeleton() : _buildCourseBlocks()),
96+
],
7897
),
7998
),
8099
],
@@ -87,7 +106,7 @@ class CourseTableGrid extends StatelessWidget {
87106
SizedBox(width: _stubWidth),
88107
for (var day in _weekDays)
89108
SizedBox(
90-
width: (viewportWidth! - _stubWidth) / _weekDays.length,
109+
width: (widget.viewportWidth! - _stubWidth) / _weekDays.length,
91110
child: AutoSizeText(
92111
day.label,
93112
textAlign: .center,
@@ -120,7 +139,7 @@ class CourseTableGrid extends StatelessWidget {
120139
),
121140
),
122141
SizedBox(
123-
width: viewportWidth! - _stubWidth,
142+
width: widget.viewportWidth! - _stubWidth,
124143
height: _periodRowHeight,
125144
child: Container(
126145
decoration: BoxDecoration(
@@ -136,8 +155,77 @@ class CourseTableGrid extends StatelessWidget {
136155
);
137156
}
138157

158+
159+
List<Widget> _bulidSkeleton() {
160+
final columnWidth = (widget.viewportWidth! - _stubWidth) / _weekDays.length;
161+
final random = Random();
162+
163+
// Track occupied slots per day to avoid overlaps
164+
final occupied = List.generate(_weekDays.length, (_) => <int>{});
165+
final blocks = <Widget>[];
166+
167+
for (var i = 0; i < 16; i++) {
168+
final dayIndex = random.nextInt(_weekDays.length);
169+
final spanLength = 2 + random.nextInt(2); // 2-3 periods
170+
final maxStart = _periods.length - spanLength;
171+
172+
// Find a non-overlapping start index
173+
int? startIndex;
174+
for (var attempt = 0; attempt < 10; attempt++) {
175+
final candidate = random.nextInt(maxStart + 1);
176+
final slots = List.generate(spanLength, (j) => candidate + j);
177+
if (slots.every((s) => !occupied[dayIndex].contains(s))) {
178+
startIndex = candidate;
179+
occupied[dayIndex].addAll(slots);
180+
break;
181+
}
182+
}
183+
if (startIndex == null) continue;
184+
185+
final blockTop = startIndex * _periodRowHeight;
186+
final blockLeft = _stubWidth + (dayIndex * columnWidth);
187+
final blockHeight = spanLength * _periodRowHeight;
188+
final delayMs = 50 + random.nextInt(101);
189+
const riseDurationMs = 350;
190+
final totalDurationMs = riseDurationMs + delayMs;
191+
final startAt = delayMs / totalDurationMs;
192+
193+
blocks.add(
194+
Positioned(
195+
key: ValueKey('skeleton-$i'),
196+
top: blockTop,
197+
left: blockLeft,
198+
child: SizedBox(
199+
width: columnWidth,
200+
height: blockHeight,
201+
child: Padding(
202+
padding: const EdgeInsets.all(2),
203+
child: TweenAnimationBuilder<double>(
204+
tween: Tween(begin: 1, end: 0),
205+
duration: Duration(milliseconds: totalDurationMs),
206+
curve: Interval(startAt, 1, curve: Curves.easeOutCubic),
207+
builder: (context, t, child) {
208+
return Opacity(
209+
opacity: 1 - t,
210+
child: Transform.translate(
211+
offset: Offset(0, 16 * t),
212+
child: child,
213+
),
214+
);
215+
},
216+
child: const CourseTableBlockSkeleton(),
217+
),
218+
),
219+
),
220+
),
221+
);
222+
}
223+
224+
return blocks;
225+
}
226+
139227
List<Widget> _buildCourseBlocks() {
140-
final columnWidth = (viewportWidth! - _stubWidth) / _weekDays.length;
228+
final columnWidth = (widget.viewportWidth! - _stubWidth) / _weekDays.length;
141229
final random = Random();
142230
const blockColors = <Color>[
143231
Colors.blue,
@@ -150,8 +238,8 @@ class CourseTableGrid extends StatelessWidget {
150238
];
151239

152240
final blocks = <Widget>[];
153-
for (var i = 0; i < couseTableSummary.courses.length; i++) {
154-
final course = couseTableSummary.courses[i];
241+
for (var i = 0; i < widget.couseTableSummary.courses.length; i++) {
242+
final course = widget.couseTableSummary.courses[i];
155243
final dayIndex = _weekDays.indexOf(course.dayOfWeek);
156244
final startIndex = _periods.indexOf(course.startSection);
157245
final endIndex = _periods.indexOf(course.endSection);
@@ -170,6 +258,7 @@ class CourseTableGrid extends StatelessWidget {
170258

171259
blocks.add(
172260
Positioned(
261+
key: ValueKey('course-$i'),
173262
top: blockTop,
174263
left: blockLeft,
175264
child: SizedBox(

0 commit comments

Comments
 (0)