@@ -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