learningtribes-studio-frontend 1.4.1 → 1.4.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "learningtribes-studio-frontend",
3
- "version": "1.4.1",
3
+ "version": "1.4.2-alpha.0",
4
4
  "description": "The frontend for the Open edX platform",
5
5
  "repository": "Learningtribes/studio-frontend",
6
6
  "scripts": {
@@ -5,25 +5,42 @@
5
5
  }
6
6
 
7
7
  .drop-zone-compact {
8
- border: $dark dashed 1px;
9
- padding: 0;
8
+ border: 1px dashed #d0d0d0;
9
+ border-radius: 8px;
10
+ padding: 24px 20px;
11
+ background-color: #fafafa;
12
+ transition: all 0.2s ease;
13
+ cursor: pointer;
14
+
15
+ &:hover {
16
+ border-color: #4a90d9;
17
+ background-color: #fff;
18
+ }
10
19
 
11
- button {
12
- white-space: normal;
13
- margin-top: $spacer;
14
- margin-bottom: $spacer;
20
+ [data-identifier="asset-drop-zone-icon"] {
21
+ color: #4a90d9;
22
+ margin-bottom: 12px;
23
+
24
+ span {
25
+ font-size: 32px;
26
+ }
15
27
  }
16
28
 
17
29
  h2 {
18
- @extend .center-text;
19
- font-weight: $font-weight-bold;
20
- font-size: $font-size-lg;
21
- margin-bottom: 0;
30
+ display: none;
31
+ }
32
+
33
+ .label-or {
34
+ display: none;
35
+ }
36
+
37
+ button.btn {
38
+ display: none;
22
39
  }
23
40
 
24
41
  p {
25
- font-size: $font-size-sm;
26
- margin-bottom: $spacer * 0.5;
42
+ font-size: 12px;
43
+ margin-bottom: 0;
27
44
  }
28
45
  }
29
46
 
@@ -31,16 +48,53 @@
31
48
  @extend .drop-zone-compact;
32
49
 
33
50
  @media (min-width: map-get($grid-breakpoints, 'md')) {
34
- padding: $spacer;
35
- h2 {
36
- margin-bottom: inherit;
37
- }
51
+ padding: 30px 20px;
38
52
  }
39
53
  }
40
54
 
41
55
  .drop-active {
42
- outline: $dark dashed 2px;
43
- background-color: rgba($primary, 0.2);
56
+ outline: none;
57
+ border: 2px dashed #4a90d9;
58
+ background-color: rgba(74, 144, 217, 0.05);
59
+ }
60
+
61
+ .upload-prompt {
62
+ text-align: center;
63
+ margin-bottom: 6px;
64
+ }
65
+
66
+ .upload-link {
67
+ background: none;
68
+ border: none;
69
+ color: #4a90d9;
70
+ font-size: 14px;
71
+ font-weight: 500;
72
+ cursor: pointer;
73
+ padding: 0;
74
+ text-decoration: none;
75
+
76
+ &:hover {
77
+ text-decoration: underline;
78
+ color: darken(#4a90d9, 10%);
79
+ }
80
+
81
+ &:focus {
82
+ outline: 2px solid rgba(74, 144, 217, 0.5);
83
+ outline-offset: 2px;
84
+ }
85
+ }
86
+
87
+ .upload-text {
88
+ color: #333;
89
+ font-size: 14px;
90
+ font-weight: 400;
91
+ }
92
+
93
+ .size-limit {
94
+ text-align: center;
95
+ color: #4a90d9;
96
+ font-size: 12px;
97
+ margin-bottom: 0;
44
98
  }
45
99
 
46
100
  .supported-files-types {
@@ -54,3 +108,19 @@
54
108
  font-size: 12px !important;
55
109
  font-weight: 400;
56
110
  }
111
+
112
+ // Collapsed zone in image modal
113
+ .collapsedZoneStyle {
114
+ .drop-zone-compact,
115
+ .drop-zone {
116
+ padding: 20px 16px;
117
+
118
+ [data-identifier="asset-drop-zone-icon"] {
119
+ margin-bottom: 8px;
120
+
121
+ span {
122
+ font-size: 28px;
123
+ }
124
+ }
125
+ }
126
+ }
@@ -8,12 +8,12 @@ const messages = defineMessages({
8
8
  },
9
9
  assetsDropZoneBrowseLabel: {
10
10
  id: 'assetsDropZoneBrowseLabel',
11
- defaultMessage: 'Select from your computer',
11
+ defaultMessage: 'Click to upload',
12
12
  description: 'Upload label for the "Browse" button',
13
13
  },
14
14
  assetsDropZoneHeader: {
15
15
  id: 'assetsDropZoneHeader',
16
- defaultMessage: 'Drag & Drop',
16
+ defaultMessage: 'or drag and drop',
17
17
  description: 'Upload with dropzone section heading',
18
18
  },
19
19
  assetsDropZoneMaxFileSizeLabel: {
@@ -37,7 +37,9 @@ export default class AssetsDropZone extends React.Component {
37
37
  };
38
38
 
39
39
  setUploadButtonRef = (ref) => {
40
- this.props.buttonRef(ref);
40
+ if (this.props.buttonRef) {
41
+ this.props.buttonRef(ref);
42
+ }
41
43
  };
42
44
 
43
45
  handleClick = () => {
@@ -66,30 +68,29 @@ export default class AssetsDropZone extends React.Component {
66
68
  <div className={styles['center-text']} data-identifier="asset-drop-zone-icon">
67
69
  <span
68
70
  aria-hidden
69
- className={'fad fa-cloud-upload-alt '+classNames(
71
+ className={'far fa-image '+classNames(
70
72
  styles['center-text'],
71
- //FontAwesomeStyles.fa,
72
- //FontAwesomeStyles['fa-cloud-upload'],
73
73
  FontAwesomeStyles['fa-3x'])}
74
74
  />
75
75
  </div>
76
- <WrappedMessage message={messages.assetsDropZoneHeader}>
77
- { displayText => (
78
- <h2 data-identifier="asset-drop-zone-header">{displayText}</h2>
79
- )}
80
- </WrappedMessage>
81
- <span className="label-or">or</span>
82
- <div className={styles['center-text']}>
76
+ <div className={styles['upload-prompt']}>
83
77
  <WrappedMessage message={messages.assetsDropZoneBrowseLabel} >
84
78
  { displayText => (
85
- <Button
79
+ <button
80
+ type="button"
86
81
  aria-describedby="asset-drop-zone-max-file-size-label"
87
- className={['btn', 'btn-outline-primary']}
82
+ className={styles['upload-link']}
88
83
  data-identifier="asset-drop-zone-browse-button"
89
- label={displayText}
90
84
  onClick={this.handleClick}
91
- inputRef={this.setUploadButtonRef}
92
- />
85
+ ref={this.setUploadButtonRef}
86
+ >
87
+ {displayText}
88
+ </button>
89
+ )}
90
+ </WrappedMessage>
91
+ <WrappedMessage message={messages.assetsDropZoneHeader}>
92
+ { displayText => (
93
+ <span className={styles['upload-text']} data-identifier="asset-drop-zone-header"> {displayText}</span>
93
94
  )}
94
95
  </WrappedMessage>
95
96
  </div>
@@ -102,7 +103,7 @@ export default class AssetsDropZone extends React.Component {
102
103
  { displayText => (
103
104
  <p
104
105
  aria-hidden
105
- className={styles['center-text']}
106
+ className={styles['size-limit']}
106
107
  data-identifier="asset-drop-zone-max-file-size-label"
107
108
  id="asset-drop-zone-max-file-size-label"
108
109
  >
@@ -1,6 +1,138 @@
1
1
  @import 'edx-bootstrap';
2
2
  @import 'utils/scss/focus';
3
3
 
4
+ // Horizontal scrollable grid
5
+ .assets-grid {
6
+ display: flex;
7
+ gap: 10px;
8
+ overflow-x: auto;
9
+ padding: 8px 0;
10
+ margin-bottom: 10px;
11
+
12
+ &:focus {
13
+ @include focus();
14
+ }
15
+
16
+ // Custom scrollbar
17
+ &::-webkit-scrollbar {
18
+ height: 6px;
19
+ }
20
+
21
+ &::-webkit-scrollbar-track {
22
+ background: #f0f0f0;
23
+ border-radius: 3px;
24
+ }
25
+
26
+ &::-webkit-scrollbar-thumb {
27
+ background: #c0c0c0;
28
+ border-radius: 3px;
29
+
30
+ &:hover {
31
+ background: #a0a0a0;
32
+ }
33
+ }
34
+ }
35
+
36
+ .asset-grid-item {
37
+ flex-shrink: 0;
38
+ width: 100px;
39
+ cursor: pointer;
40
+ transition: all 0.2s ease;
41
+
42
+ &:hover {
43
+ .asset-preview {
44
+ border-color: #c0c0c0;
45
+ }
46
+ }
47
+
48
+ &.selected {
49
+ .asset-preview {
50
+ border-color: #4a90d9;
51
+ border-width: 2px;
52
+ }
53
+
54
+ .asset-name {
55
+ color: #4a90d9;
56
+ background-color: rgba(74, 144, 217, 0.1);
57
+ padding: 2px 4px;
58
+ border-radius: 4px;
59
+ }
60
+ }
61
+ }
62
+
63
+ .asset-preview {
64
+ width: 100px;
65
+ height: 100px;
66
+ border: 1px solid #e0e0e0;
67
+ border-radius: 8px;
68
+ background-color: #fff;
69
+ display: flex;
70
+ flex-direction: column;
71
+ align-items: center;
72
+ justify-content: center;
73
+ position: relative;
74
+ margin-bottom: 8px;
75
+ transition: border-color 0.2s ease;
76
+ overflow: hidden;
77
+ }
78
+
79
+ .file-type-badge {
80
+ position: absolute;
81
+ top: 6px;
82
+ left: 6px;
83
+ background-color: #e8f4fd;
84
+ color: #1976d2;
85
+ font-size: 10px;
86
+ font-weight: 700;
87
+ padding: 2px 6px;
88
+ border-radius: 4px;
89
+ text-transform: lowercase;
90
+ z-index: 1;
91
+ }
92
+
93
+ .asset-thumbnail {
94
+ width: 100%;
95
+ height: 100%;
96
+ object-fit: cover;
97
+ }
98
+
99
+ .asset-icon {
100
+ font-size: 36px;
101
+ color: #9e9e9e;
102
+
103
+ i {
104
+ display: block;
105
+ }
106
+ }
107
+
108
+ .asset-info {
109
+ text-align: center;
110
+ }
111
+
112
+ .asset-name {
113
+ font-size: 12px;
114
+ font-weight: 500;
115
+ color: #333;
116
+ white-space: nowrap;
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ max-width: 100px;
120
+ margin-bottom: 2px;
121
+
122
+ &.selected {
123
+ color: #4a90d9;
124
+ background-color: rgba(74, 144, 217, 0.1);
125
+ padding: 2px 4px;
126
+ border-radius: 4px;
127
+ }
128
+ }
129
+
130
+ .asset-size {
131
+ font-size: 11px;
132
+ color: #666;
133
+ }
134
+
135
+ // Old styles
4
136
  .assets-list-image-preview-container {
5
137
  background-color: theme-color('light');
6
138
  color: theme-color('dark');
@@ -72,57 +72,64 @@ export default class AssetsList extends React.Component {
72
72
  }
73
73
  }
74
74
 
75
- getAssetsListHeader = () => (
76
- <div className={classNames(styles['list-header'], 'row')}>
77
- <span className="col-6 offset-3" data-identifier="asset-file-name" id="name-header"><WrappedMessage message={messages.assetsListNameLabel} /></span>
78
- <span className="col-3" data-identifier="asset-date-added" id="date-added-header"><WrappedMessage message={messages.assetsListDateLabel} /></span>
79
- </div>
80
- )
81
-
82
- getAssetListItem = (asset, index) => {
75
+ getFileExtension = (displayName) => {
76
+ if (!displayName) return '';
77
+ const lastDot = displayName.lastIndexOf('.');
78
+ if (lastDot === -1 || lastDot === displayName.length - 1) {
79
+ return 'file';
80
+ }
81
+ return displayName.substring(lastDot + 1).toLowerCase();
82
+ }
83
+
84
+ formatFileSize = (bytes) => {
85
+ if (!bytes || bytes === 0) return '';
86
+ const units = ['B', 'KB', 'MB', 'GB'];
87
+ const k = 1024;
88
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
89
+ const size = (bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1);
90
+ return `${size} ${units[i]}`;
91
+ }
92
+
93
+ getAssetGridItem = (asset, index) => {
83
94
  const isSelected = this.props.selectedAsset.id === asset.id;
95
+ const baseUrl = this.props.courseDetails.base_url || '';
96
+ const fileExt = this.getFileExtension(asset.display_name);
97
+ const fileSize = this.formatFileSize(asset.file_size);
84
98
 
85
99
  return (
86
- <li
87
- // prevent clicking on list item from triggering parent's focus
100
+ <div
88
101
  onMouseDown={(e) => { e.preventDefault(); this.onAssetClick(asset, index); }}
89
- className={classNames('list-group-item list-group-item-action', { active: isSelected })}
102
+ className={classNames(styles['asset-grid-item'], { [styles.selected]: isSelected })}
90
103
  id={`asset-list-option-${index}`}
91
104
  aria-selected={isSelected}
92
105
  role="option"
93
- key={asset.display_name}
106
+ key={asset.id || asset.display_name}
94
107
  tabIndex="-1"
95
- aria-labelledby={`name-header asset-name-${index} date-added-header asset-date-${index}`}
96
108
  >
97
- <div className="row">
98
- {this.getThumbnailElement(asset.thumbnail)}
99
- {this.getDisplayNameElement(asset.display_name, index)}
100
- {this.getDateAddedElement(asset.date_added, index)}
109
+ <div className={styles['asset-preview']}>
110
+ <span className={styles['file-type-badge']}>{fileExt}</span>
111
+ {asset.thumbnail ? (
112
+ <img
113
+ className={styles['asset-thumbnail']}
114
+ src={`${baseUrl}${asset.thumbnail}`}
115
+ alt=""
116
+ />
117
+ ) : (
118
+ <div className={styles['asset-icon']}>
119
+ <i className="far fa-image" />
120
+ </div>
121
+ )}
122
+ </div>
123
+ <div className={styles['asset-info']}>
124
+ <div className={classNames(styles['asset-name'], { [styles.selected]: isSelected })} title={asset.display_name}>
125
+ {asset.display_name}
126
+ </div>
127
+ {fileSize && (
128
+ <div className={styles['asset-size']}>
129
+ {fileSize}
130
+ </div>
131
+ )}
101
132
  </div>
102
- </li>
103
- );
104
- }
105
-
106
- getDateAddedElement = (dateAdded, index) => (
107
- <span id={`asset-date-${index}`} className="col-3" data-identifier="asset-date-added">{dateAdded}</span>
108
- );
109
-
110
- getDisplayNameElement = (displayName, index) => (
111
- <span id={`asset-name-${index}`} className="col-6" data-identifier="asset-file-name">{displayName}</span>
112
- );
113
-
114
- getImageThumbnail(thumbnail) {
115
- const baseUrl = this.props.courseDetails.base_url || '';
116
-
117
- return (
118
- <div className={styles['assets-list-image-preview-container']}>
119
- {
120
- thumbnail ?
121
- <img className={styles['assets-list-image-preview-image']} src={`${baseUrl}${thumbnail}`} alt="" data-identifier="asset-image-thumbnail" /> :
122
- <WrappedMessage message={messages.assetsListNoPreview} >
123
- {displayText => <span className={classNames('text-center')} data-identifier="asset-image-thumbnail">{displayText}</span>}
124
- </WrappedMessage>
125
- }
126
133
  </div>
127
134
  );
128
135
  }
@@ -130,17 +137,11 @@ export default class AssetsList extends React.Component {
130
137
  getSelectedAssetIndex = (assetsList, selectedAsset) => (assetsList.findIndex(
131
138
  asset => asset.id === selectedAsset.id));
132
139
 
133
- getThumbnailElement = thumbnail => (
134
- <span aria-hidden className="col">{this.getImageThumbnail(thumbnail)}</span>
135
- );
136
-
137
140
  handleKeyDown = (e) => {
138
141
  switch (e.key) {
139
- case 'ArrowDown': {
140
- // prevent scrolling entire modal body with arrow keys
142
+ case 'ArrowRight': {
141
143
  e.preventDefault();
142
-
143
- if (this.state.selectedAssetIndex < this.props.paginationMetadata.pageSize - 1) {
144
+ if (this.state.selectedAssetIndex < this.props.assetsList.length - 1) {
144
145
  this.setState(
145
146
  (state, props) => ({
146
147
  selectedAssetIndex: this.state.selectedAssetIndex + 1,
@@ -151,10 +152,8 @@ export default class AssetsList extends React.Component {
151
152
  }
152
153
  break;
153
154
  }
154
- case 'ArrowUp': {
155
- // prevent scrolling entire modal body with arrow keys
155
+ case 'ArrowLeft': {
156
156
  e.preventDefault();
157
-
158
157
  if (this.state.selectedAssetIndex > 0) {
159
158
  this.setState(
160
159
  (state, props) => ({
@@ -171,29 +170,36 @@ export default class AssetsList extends React.Component {
171
170
  }
172
171
 
173
172
  render = () => {
174
- const assetsListItems = this.props.assetsList.map((asset, index) =>
175
- this.getAssetListItem(asset, index));
173
+ const assetsGridItems = this.props.assetsList.map((asset, index) =>
174
+ this.getAssetGridItem(asset, index));
176
175
 
177
176
  return (
178
- <React.Fragment>
179
- {this.getAssetsListHeader()}
180
- <ol
181
- aria-activedescendant={this.state.selectedAssetIndex > -1 ? `asset-list-option-${this.state.selectedAssetIndex}` : null}
182
- className="list-group"
183
- onFocus={this.onListBoxFocus}
184
- onKeyDown={this.handleKeyDown}
185
- role="listbox"
186
- tabIndex="0"
187
- >
188
- {assetsListItems}
189
- </ol>
190
- </React.Fragment>
177
+ <div
178
+ aria-activedescendant={this.state.selectedAssetIndex > -1 ? `asset-list-option-${this.state.selectedAssetIndex}` : null}
179
+ className={styles['assets-grid']}
180
+ onFocus={this.onListBoxFocus}
181
+ onKeyDown={this.handleKeyDown}
182
+ role="listbox"
183
+ tabIndex="0"
184
+ >
185
+ {assetsGridItems}
186
+ </div>
191
187
  );
192
188
  };
193
189
  }
194
190
 
195
191
  AssetsList.propTypes = {
196
- assetsList: PropTypes.arrayOf(PropTypes.object).isRequired,
192
+ assetsList: PropTypes.arrayOf(PropTypes.shape({
193
+ display_name: PropTypes.string,
194
+ content_type: PropTypes.string,
195
+ url: PropTypes.string,
196
+ date_added: PropTypes.string,
197
+ id: PropTypes.string,
198
+ portable_url: PropTypes.string,
199
+ thumbnail: PropTypes.string,
200
+ external_url: PropTypes.string,
201
+ file_size: PropTypes.number,
202
+ })).isRequired,
197
203
  courseDetails: PropTypes.shape({
198
204
  lang: PropTypes.string,
199
205
  url_name: PropTypes.string,
@@ -222,5 +228,6 @@ AssetsList.propTypes = {
222
228
  portable_url: PropTypes.string,
223
229
  thumbnail: PropTypes.string,
224
230
  external_url: PropTypes.string,
231
+ file_size: PropTypes.number,
225
232
  }).isRequired,
226
233
  };
@@ -76,3 +76,73 @@
76
76
  .invalid-feedback {
77
77
  font-size: $font-size-base;
78
78
  }
79
+
80
+ .modal-body {
81
+ // Form fields
82
+ form {
83
+ .paragon-fieldset {
84
+ margin-bottom: 1.5rem;
85
+
86
+ fieldset {
87
+ legend {
88
+ font-size: 14px;
89
+ font-weight: 500;
90
+ color: #333;
91
+ margin-bottom: 0.5rem;
92
+ }
93
+ }
94
+ }
95
+
96
+ .form-group {
97
+ margin-bottom: 1rem;
98
+
99
+ label {
100
+ font-size: 14px;
101
+ font-weight: 500;
102
+ color: #333;
103
+ }
104
+
105
+ .form-control {
106
+ border-radius: 8px;
107
+ border: 1px solid #e0e0e0;
108
+ padding: 10px 14px;
109
+ font-size: 14px;
110
+
111
+ &:focus {
112
+ border-color: #4a90d9;
113
+ box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.1);
114
+ }
115
+ }
116
+
117
+ .form-text {
118
+ font-size: 12px;
119
+ color: #666;
120
+ margin-top: 4px;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Dimensions row
126
+ .form-row {
127
+ .col {
128
+ .form-group {
129
+ label {
130
+ font-size: 14px;
131
+ font-weight: 500;
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Checkbox
139
+ .form-check {
140
+ .form-check-input {
141
+ margin-top: 0.25rem;
142
+ }
143
+
144
+ .form-check-label {
145
+ font-size: 14px;
146
+ color: #333;
147
+ }
148
+ }
@@ -83,8 +83,8 @@ const messages = defineMessages({
83
83
  },
84
84
  editImageModalInsertImageButton: {
85
85
  id: 'editImageModalInsertImageButton',
86
- defaultMessage: 'Insert Image',
87
- description: 'Text inside the Insert Image button at the bottom of the modal',
86
+ defaultMessage: 'Save',
87
+ description: 'Text inside the Save button at the bottom of the modal',
88
88
  },
89
89
  editImageModalLearnMore: {
90
90
  id: 'editImageModalLearnMore',