PrometheusGroup commited on
Commit
0df20ca
·
verified ·
1 Parent(s): 6ce77d4

use dark, moder, clean CSS and HTML5, use separate html and python files, draggable mini-player, with play/pause next/previous random shuffle icon, video "x of y", main page has a title header, search bar, under the search bar is a flex-grid (variable zoom upto 8x8 cards - use "+" and "-" buttons) displaying search results using CSS cards, video thumbnail, duration, video title, video author or channel name, green background signifying it is downloaded, each card will have a remove "X" circle button on the lower right corner, when user enters text and clicks search a modal opens and displays the results in a search grid (this search grid is to be the same format as the playlist flex-grid - css cards will have a red background for 'not downloaded', a remove "x" circle button to remove it from the search results list, user can click a card to add it to the playlist when selected a card in the search results display gets a green glowing border. Search modal needs a 'back' button, returning the user to the playlist, where they can press 'download new' to start the downloads. Theuser must be able to acess the search AND playlist while a video is playing. apply best practice to padding, margins, and layout.

Browse files
Files changed (4) hide show
  1. app.py +80 -0
  2. player.html +210 -0
  3. script.js +120 -9
  4. style.css +131 -2
app.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```python
2
+ from flask import Flask, render_template, request, jsonify
3
+
4
+ app = Flask(__name__)
5
+
6
+ # Mock data for demonstration
7
+ mock_playlist = [
8
+ {
9
+ "id": 1,
10
+ "title": "Midnight City",
11
+ "artist": "M83",
12
+ "duration": "4:03",
13
+ "thumbnail": "http://static.photos/music/200x200/1",
14
+ "downloaded": True
15
+ },
16
+ {
17
+ "id": 2,
18
+ "title": "Blinding Lights",
19
+ "artist": "The Weeknd",
20
+ "duration": "3:20",
21
+ "thumbnail": "http://static.photos/music/200x200/2",
22
+ "downloaded": True
23
+ }
24
+ ]
25
+
26
+ mock_search_results = [
27
+ {
28
+ "id": 3,
29
+ "title": "Save Your Tears",
30
+ "artist": "The Weeknd",
31
+ "duration": "3:35",
32
+ "thumbnail": "http://static.photos/music/200x200/3",
33
+ "downloaded": False
34
+ },
35
+ {
36
+ "id": 4,
37
+ "title": "Starboy",
38
+ "artist": "The Weeknd ft. Daft Punk",
39
+ "duration": "3:50",
40
+ "thumbnail": "http://static.photos/music/200x200/4",
41
+ "downloaded": False
42
+ }
43
+ ]
44
+
45
+ @app.route('/')
46
+ def index():
47
+ return render_template('player.html')
48
+
49
+ @app.route('/api/playlist')
50
+ def get_playlist():
51
+ return jsonify(mock_playlist)
52
+
53
+ @app.route('/api/search', methods=['POST'])
54
+ def search():
55
+ query = request.json.get('query', '')
56
+ # In a real app, you would search your database or API here
57
+ return jsonify(mock_search_results)
58
+
59
+ @app.route('/api/add_to_playlist', methods=['POST'])
60
+ def add_to_playlist():
61
+ song_id = request.json.get('id')
62
+ # Find the song in search results and add to playlist
63
+ for song in mock_search_results:
64
+ if song['id'] == song_id:
65
+ new_song = song.copy()
66
+ new_song['downloaded'] = False
67
+ mock_playlist.append(new_song)
68
+ return jsonify({"success": True})
69
+ return jsonify({"success": False}), 404
70
+
71
+ @app.route('/api/remove_from_playlist', methods=['POST'])
72
+ def remove_from_playlist():
73
+ song_id = request.json.get('id')
74
+ global mock_playlist
75
+ mock_playlist = [song for song in mock_playlist if song['id'] != song_id]
76
+ return jsonify({"success": True})
77
+
78
+ if __name__ == '__main__':
79
+ app.run(debug=True)
80
+ ```
player.html ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VoidScape Player</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://unpkg.com/feather-icons"></script>
10
+ <script>
11
+ tailwind.config = {
12
+ darkMode: 'class',
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ primary: {
17
+ 50: '#f0f9ff',
18
+ 100: '#e0f2fe',
19
+ 200: '#bae6fd',
20
+ 300: '#7dd3fc',
21
+ 400: '#38bdf8',
22
+ 500: '#0ea5e9',
23
+ 600: '#0284c7',
24
+ 700: '#0369a1',
25
+ 800: '#075985',
26
+ 900: '#0c4a6e',
27
+ },
28
+ secondary: {
29
+ 50: '#f5f3ff',
30
+ 100: '#ede9fe',
31
+ 200: '#ddd6fe',
32
+ 300: '#c4b5fd',
33
+ 400: '#a78bfa',
34
+ 500: '#8b5cf6',
35
+ 600: '#7c3aed',
36
+ 700: '#6d28d9',
37
+ 800: '#5b21b6',
38
+ 900: '#4c1d95',
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+ </head>
46
+ <body class="bg-gray-900 text-gray-100 min-h-screen">
47
+ <custom-navbar></custom-navbar>
48
+
49
+ <main class="container mx-auto px-4 py-12">
50
+ <div class="max-w-6xl mx-auto">
51
+ <h1 class="text-4xl font-bold mb-8 text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-secondary-500">
52
+ VoidScape Player
53
+ </h1>
54
+
55
+ <div class="relative mb-8">
56
+ <input type="text" id="search-input" placeholder="Search for music..."
57
+ class="w-full bg-gray-800 border border-gray-700 rounded-lg px-6 py-4 text-lg focus:outline-none focus:ring-2 focus:ring-primary-500">
58
+ <button id="search-btn" class="absolute right-3 top-1/2 transform -translate-y-1/2 bg-primary-600 hover:bg-primary-700 rounded-lg px-4 py-2 transition-all">
59
+ Search
60
+ </button>
61
+ </div>
62
+
63
+ <div class="flex justify-between items-center mb-6">
64
+ <h2 class="text-2xl font-semibold">Your Playlist</h2>
65
+ <div class="flex gap-2">
66
+ <button id="zoom-out" class="p-2 bg-gray-800 rounded-lg hover:bg-gray-700">
67
+ <i data-feather="minus"></i>
68
+ </button>
69
+ <button id="zoom-in" class="p-2 bg-gray-800 rounded-lg hover:bg-gray-700">
70
+ <i data-feather="plus"></i>
71
+ </button>
72
+ </div>
73
+ </div>
74
+
75
+ <div id="playlist-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
76
+ <!-- Playlist items will be inserted here -->
77
+ </div>
78
+ </div>
79
+ </main>
80
+
81
+ <!-- Search Modal -->
82
+ <div id="search-modal" class="fixed inset-0 bg-black bg-opacity-90 z-50 hidden overflow-y-auto">
83
+ <div class="container mx-auto px-4 py-12">
84
+ <div class="flex justify-between items-center mb-8">
85
+ <h2 class="text-2xl font-semibold">Search Results</h2>
86
+ <button id="back-btn" class="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg">
87
+ Back to Playlist
88
+ </button>
89
+ </div>
90
+
91
+ <div id="search-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
92
+ <!-- Search results will be inserted here -->
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Mini Player -->
98
+ <div id="mini-player" class="fixed bottom-4 right-4 bg-gray-800 border border-gray-700 rounded-lg shadow-xl w-80 overflow-hidden draggable">
99
+ <div class="p-4 cursor-move">
100
+ <div class="flex items-center gap-4">
101
+ <img id="mini-player-thumb" src="http://static.photos/music/200x200/1" class="w-12 h-12 rounded-lg object-cover">
102
+ <div class="flex-1 min-w-0">
103
+ <h3 id="mini-player-title" class="font-medium truncate">Song Title</h3>
104
+ <p id="mini-player-artist" class="text-sm text-gray-400 truncate">Artist Name</p>
105
+ </div>
106
+ <div class="flex items-center gap-2">
107
+ <span id="mini-player-count" class="text-sm text-gray-400">1/10</span>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="flex justify-between items-center mt-4">
112
+ <button id="shuffle-btn" class="p-2 text-gray-400 hover:text-white">
113
+ <i data-feather="shuffle"></i>
114
+ </button>
115
+ <button id="prev-btn" class="p-2 text-gray-400 hover:text-white">
116
+ <i data-feather="skip-back"></i>
117
+ </button>
118
+ <button id="play-btn" class="p-2 bg-primary-600 hover:bg-primary-700 rounded-full">
119
+ <i data-feather="play"></i>
120
+ </button>
121
+ <button id="next-btn" class="p-2 text-gray-400 hover:text-white">
122
+ <i data-feather="skip-forward"></i>
123
+ </button>
124
+ <button id="close-player" class="p-2 text-gray-400 hover:text-white">
125
+ <i data-feather="x"></i>
126
+ </button>
127
+ </div>
128
+
129
+ <div class="mt-2 flex items-center gap-2">
130
+ <span class="text-xs text-gray-400">0:00</span>
131
+ <div class="flex-1 h-1 bg-gray-700 rounded-full">
132
+ <div id="progress-bar" class="h-full bg-primary-500 rounded-full w-0"></div>
133
+ </div>
134
+ <span class="text-xs text-gray-400">3:45</span>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <script src="components/navbar.js"></script>
140
+ <script src="script.js"></script>
141
+ <script>
142
+ feather.replace();
143
+
144
+ // Make mini-player draggable
145
+ const player = document.getElementById('mini-player');
146
+ let isDragging = false;
147
+ let offsetX, offsetY;
148
+
149
+ player.addEventListener('mousedown', (e) => {
150
+ if (e.target.closest('button')) return;
151
+
152
+ isDragging = true;
153
+ offsetX = e.clientX - player.getBoundingClientRect().left;
154
+ offsetY = e.clientY - player.getBoundingClientRect().top;
155
+ player.style.cursor = 'grabbing';
156
+ });
157
+
158
+ document.addEventListener('mousemove', (e) => {
159
+ if (!isDragging) return;
160
+
161
+ player.style.left = `${e.clientX - offsetX}px`;
162
+ player.style.top = `${e.clientY - offsetY}px`;
163
+ });
164
+
165
+ document.addEventListener('mouseup', () => {
166
+ isDragging = false;
167
+ player.style.cursor = 'grab';
168
+ });
169
+
170
+ // Modal functionality
171
+ const searchBtn = document.getElementById('search-btn');
172
+ const searchModal = document.getElementById('search-modal');
173
+ const backBtn = document.getElementById('back-btn');
174
+
175
+ searchBtn.addEventListener('click', () => {
176
+ searchModal.classList.remove('hidden');
177
+ // Here you would fetch search results and populate the search grid
178
+ });
179
+
180
+ backBtn.addEventListener('click', () => {
181
+ searchModal.classList.add('hidden');
182
+ });
183
+
184
+ // Zoom functionality
185
+ const zoomIn = document.getElementById('zoom-in');
186
+ const zoomOut = document.getElementById('zoom-out');
187
+ const playlistGrid = document.getElementById('playlist-grid');
188
+
189
+ let zoomLevel = 5; // Default to 5 columns
190
+
191
+ zoomIn.addEventListener('click', () => {
192
+ if (zoomLevel < 8) {
193
+ zoomLevel++;
194
+ updateGridColumns();
195
+ }
196
+ });
197
+
198
+ zoomOut.addEventListener('click', () => {
199
+ if (zoomLevel > 2) {
200
+ zoomLevel--;
201
+ updateGridColumns();
202
+ }
203
+ });
204
+
205
+ function updateGridColumns() {
206
+ playlistGrid.className = `grid gap-4 grid-cols-${zoomLevel}`;
207
+ }
208
+ </script>
209
+ </body>
210
+ </html>
script.js CHANGED
@@ -1,3 +1,4 @@
 
1
  document.addEventListener('DOMContentLoaded', () => {
2
  // Initialize animations
3
  const animatedElements = document.querySelectorAll('.fade-in, .slide-up');
@@ -15,12 +16,122 @@ document.addEventListener('DOMContentLoaded', () => {
15
 
16
  animatedElements.forEach(el => observer.observe(el));
17
 
18
- // Theme toggle functionality
19
- const themeToggle = document.getElementById('theme-toggle');
20
- if (themeToggle) {
21
- themeToggle.addEventListener('click', () => {
22
- document.documentElement.classList.toggle('dark');
23
- localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
24
- });
25
- }
26
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
  document.addEventListener('DOMContentLoaded', () => {
3
  // Initialize animations
4
  const animatedElements = document.querySelectorAll('.fade-in, .slide-up');
 
16
 
17
  animatedElements.forEach(el => observer.observe(el));
18
 
19
+ // Load playlist
20
+ fetch('/api/playlist')
21
+ .then(response => response.json())
22
+ .then(data => renderPlaylist(data));
23
+
24
+ // Search functionality
25
+ document.getElementById('search-btn').addEventListener('click', () => {
26
+ const query = document.getElementById('search-input').value.trim();
27
+ if (query) {
28
+ fetch('/api/search', {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json'
32
+ },
33
+ body: JSON.stringify({ query })
34
+ })
35
+ .then(response => response.json())
36
+ .then(data => renderSearchResults(data));
37
+ }
38
+ });
39
+
40
+ // Playlist item click handler
41
+ document.getElementById('playlist-grid').addEventListener('click', (e) => {
42
+ if (e.target.classList.contains('remove-btn') || e.target.closest('.remove-btn')) {
43
+ const card = e.target.closest('.music-card');
44
+ const id = parseInt(card.dataset.id);
45
+ removeFromPlaylist(id);
46
+ }
47
+ });
48
+
49
+ // Search result click handler
50
+ document.getElementById('search-grid').addEventListener('click', (e) => {
51
+ const card = e.target.closest('.music-card');
52
+ if (!card) return;
53
+
54
+ if (e.target.classList.contains('remove-btn') || e.target.closest('.remove-btn')) {
55
+ card.remove();
56
+ } else {
57
+ const id = parseInt(card.dataset.id);
58
+ addToPlaylist(id);
59
+ card.classList.add('selected');
60
+ setTimeout(() => card.classList.remove('selected'), 1000);
61
+ }
62
+ });
63
+ });
64
+
65
+ function renderPlaylist(songs) {
66
+ const grid = document.getElementById('playlist-grid');
67
+ grid.innerHTML = '';
68
+
69
+ songs.forEach(song => {
70
+ const card = createMusicCard(song, true);
71
+ grid.appendChild(card);
72
+ });
73
+ }
74
+
75
+ function renderSearchResults(songs) {
76
+ const grid = document.getElementById('search-grid');
77
+ grid.innerHTML = '';
78
+
79
+ songs.forEach(song => {
80
+ const card = createMusicCard(song, false);
81
+ grid.appendChild(card);
82
+ });
83
+ }
84
+
85
+ function createMusicCard(song, isPlaylist) {
86
+ const card = document.createElement('div');
87
+ card.className = `music-card ${song.downloaded ? 'downloaded' : 'not-downloaded'}`;
88
+ card.dataset.id = song.id;
89
+
90
+ card.innerHTML = `
91
+ <img src="${song.thumbnail}" class="thumbnail">
92
+ <span class="duration">${song.duration}</span>
93
+ ${isPlaylist ? '<div class="remove-btn"><i data-feather="x"></i></div>' : ''}
94
+ <div class="info">
95
+ <div class="title">${song.title}</div>
96
+ <div class="artist">${song.artist}</div>
97
+ </div>
98
+ `;
99
+
100
+ return card;
101
+ }
102
+
103
+ function addToPlaylist(id) {
104
+ fetch('/api/add_to_playlist', {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Content-Type': 'application/json'
108
+ },
109
+ body: JSON.stringify({ id })
110
+ })
111
+ .then(response => response.json())
112
+ .then(data => {
113
+ if (data.success) {
114
+ fetch('/api/playlist')
115
+ .then(response => response.json())
116
+ .then(data => renderPlaylist(data));
117
+ }
118
+ });
119
+ }
120
+
121
+ function removeFromPlaylist(id) {
122
+ fetch('/api/remove_from_playlist', {
123
+ method: 'POST',
124
+ headers: {
125
+ 'Content-Type': 'application/json'
126
+ },
127
+ body: JSON.stringify({ id })
128
+ })
129
+ .then(response => response.json())
130
+ .then(data => {
131
+ if (data.success) {
132
+ fetch('/api/playlist')
133
+ .then(response => response.json())
134
+ .then(data => renderPlaylist(data));
135
+ }
136
+ });
137
+ }
style.css CHANGED
@@ -24,7 +24,6 @@ body {
24
  ::-webkit-scrollbar-thumb:hover {
25
  background: #444;
26
  }
27
-
28
  /* Animation classes */
29
  .fade-in {
30
  animation: fadeIn 0.5s ease-in-out;
@@ -48,4 +47,134 @@ body {
48
  opacity: 1;
49
  transform: translateY(0);
50
  }
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ::-webkit-scrollbar-thumb:hover {
25
  background: #444;
26
  }
 
27
  /* Animation classes */
28
  .fade-in {
29
  animation: fadeIn 0.5s ease-in-out;
 
47
  opacity: 1;
48
  transform: translateY(0);
49
  }
50
+ }
51
+
52
+ /* Player styles */
53
+ #mini-player {
54
+ transition: transform 0.2s;
55
+ z-index: 100;
56
+ }
57
+
58
+ #mini-player:hover {
59
+ transform: scale(1.02);
60
+ }
61
+
62
+ .draggable {
63
+ cursor: grab;
64
+ user-select: none;
65
+ }
66
+
67
+ /* Card styles */
68
+ .music-card {
69
+ position: relative;
70
+ background: #1e293b;
71
+ border-radius: 0.75rem;
72
+ overflow: hidden;
73
+ transition: all 0.3s ease;
74
+ }
75
+
76
+ .music-card:hover {
77
+ transform: translateY(-4px);
78
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
79
+ }
80
+
81
+ .music-card.downloaded {
82
+ background: linear-gradient(to bottom right, #1e293b, #14532d);
83
+ }
84
+
85
+ .music-card.not-downloaded {
86
+ background: linear-gradient(to bottom right, #1e293b, #7f1d1d);
87
+ }
88
+
89
+ .music-card.selected {
90
+ box-shadow: 0 0 0 2px #4ade80;
91
+ }
92
+
93
+ .music-card .thumbnail {
94
+ width: 100%;
95
+ aspect-ratio: 1;
96
+ object-fit: cover;
97
+ }
98
+
99
+ .music-card .duration {
100
+ position: absolute;
101
+ top: 0.5rem;
102
+ right: 0.5rem;
103
+ background: rgba(0, 0, 0, 0.7);
104
+ padding: 0.25rem 0.5rem;
105
+ border-radius: 0.25rem;
106
+ font-size: 0.75rem;
107
+ }
108
+
109
+ .music-card .remove-btn {
110
+ position: absolute;
111
+ bottom: 0.5rem;
112
+ right: 0.5rem;
113
+ width: 1.5rem;
114
+ height: 1.5rem;
115
+ background: rgba(239, 68, 68, 0.9);
116
+ border-radius: 50%;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ cursor: pointer;
121
+ transition: all 0.2s;
122
+ }
123
+
124
+ .music-card .remove-btn:hover {
125
+ transform: scale(1.1);
126
+ }
127
+
128
+ .music-card .info {
129
+ padding: 0.75rem;
130
+ }
131
+
132
+ .music-card .title {
133
+ font-weight: 500;
134
+ margin-bottom: 0.25rem;
135
+ white-space: nowrap;
136
+ overflow: hidden;
137
+ text-overflow: ellipsis;
138
+ }
139
+
140
+ .music-card .artist {
141
+ font-size: 0.875rem;
142
+ color: #94a3b8;
143
+ white-space: nowrap;
144
+ overflow: hidden;
145
+ text-overflow: ellipsis;
146
+ }
147
+
148
+ /* Modal styles */
149
+ #search-modal {
150
+ backdrop-filter: blur(5px);
151
+ }
152
+
153
+ /* Grid columns */
154
+ .grid-cols-2 {
155
+ grid-template-columns: repeat(2, minmax(0, 1fr));
156
+ }
157
+
158
+ .grid-cols-3 {
159
+ grid-template-columns: repeat(3, minmax(0, 1fr));
160
+ }
161
+
162
+ .grid-cols-4 {
163
+ grid-template-columns: repeat(4, minmax(0, 1fr));
164
+ }
165
+
166
+ .grid-cols-5 {
167
+ grid-template-columns: repeat(5, minmax(0, 1fr));
168
+ }
169
+
170
+ .grid-cols-6 {
171
+ grid-template-columns: repeat(6, minmax(0, 1fr));
172
+ }
173
+
174
+ .grid-cols-7 {
175
+ grid-template-columns: repeat(7, minmax(0, 1fr));
176
+ }
177
+
178
+ .grid-cols-8 {
179
+ grid-template-columns: repeat(8, minmax(0, 1fr));
180
+ }