Tristan Yu commited on
Commit
2186eb8
·
1 Parent(s): af7b38a

Add separate 'Add New Image' function for week 3+ with size and alignment options

Browse files
client/src/pages/WeeklyPractice.tsx CHANGED
@@ -24,6 +24,8 @@ interface WeeklyPractice {
24
  translationBrief?: string;
25
  imageUrl?: string;
26
  imageAlt?: string;
 
 
27
  }
28
 
29
  interface WeeklyPracticeWeek {
@@ -96,6 +98,7 @@ const WeeklyPractice: React.FC = () => {
96
  const [editingPractice, setEditingPractice] = useState<string | null>(null);
97
  const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
98
  const [addingPractice, setAddingPractice] = useState<boolean>(false);
 
99
  const [editForm, setEditForm] = useState<{
100
  content: string;
101
  translationBrief: string;
@@ -107,6 +110,17 @@ const WeeklyPractice: React.FC = () => {
107
  imageUrl: '',
108
  imageAlt: ''
109
  });
 
 
 
 
 
 
 
 
 
 
 
110
  const [saving, setSaving] = useState(false);
111
  const [uploading, setUploading] = useState(false);
112
  const navigate = useNavigate();
@@ -1048,6 +1062,26 @@ const WeeklyPractice: React.FC = () => {
1048
  });
1049
  };
1050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  const saveNewPractice = async () => {
1052
  try {
1053
  setSaving(true);
@@ -1101,6 +1135,50 @@ const WeeklyPractice: React.FC = () => {
1101
  }
1102
  };
1103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  const deletePractice = async (practiceId: string) => {
1105
  const user = JSON.parse(localStorage.getItem('user') || '{}');
1106
 
@@ -1674,6 +1752,113 @@ const WeeklyPractice: React.FC = () => {
1674
  )}
1675
  </div>
1676
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
  ) : (
1678
  <div className="bg-gradient-to-r from-orange-50 to-amber-50 rounded-xl p-6 border border-orange-200 border-dashed">
1679
  <div className="flex items-center justify-between">
@@ -1686,13 +1871,24 @@ const WeeklyPractice: React.FC = () => {
1686
  <p className="text-orange-700 text-sm">Create a new practice example for Week {selectedWeek}</p>
1687
  </div>
1688
  </div>
1689
- <button
1690
- onClick={startAddingPractice}
1691
- className="bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl"
1692
- >
1693
- <PlusIcon className="h-5 w-5" />
1694
- <span className="font-medium">Add Practice</span>
1695
- </button>
 
 
 
 
 
 
 
 
 
 
 
1696
  </div>
1697
  </div>
1698
  )}
@@ -1830,16 +2026,32 @@ const WeeklyPractice: React.FC = () => {
1830
  ) : (
1831
  <div>
1832
  {practice.imageUrl && selectedWeek >= 3 ? (
1833
- <div className="flex flex-col md:flex-row gap-6 items-start">
1834
- {/* Image on the left - 50% width */}
1835
- <div className="w-full md:w-1/2 flex justify-center">
 
 
 
 
 
 
 
 
 
 
 
 
1836
  {practice.imageUrl.startsWith('data:') ? (
1837
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
1838
  <img
1839
  src={practice.imageUrl}
1840
  alt={practice.imageAlt || 'Uploaded image'}
1841
  className="w-full h-auto"
1842
- style={{ height: '200px', width: 'auto', objectFit: 'contain' }}
 
 
 
 
1843
  onError={(e) => {
1844
  console.error('Error loading image:', e);
1845
  e.currentTarget.style.display = 'none';
@@ -1860,10 +2072,16 @@ const WeeklyPractice: React.FC = () => {
1860
  </div>
1861
  )}
1862
  </div>
1863
- {/* Text on the right - 50% width */}
1864
- <div className="w-full md:w-1/2">
1865
- <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
1866
- </div>
 
 
 
 
 
 
1867
  </div>
1868
  ) : (
1869
  <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
 
24
  translationBrief?: string;
25
  imageUrl?: string;
26
  imageAlt?: string;
27
+ imageSize?: number;
28
+ imageAlignment?: 'left' | 'center' | 'right';
29
  }
30
 
31
  interface WeeklyPracticeWeek {
 
98
  const [editingPractice, setEditingPractice] = useState<string | null>(null);
99
  const [editingBrief, setEditingBrief] = useState<{[key: number]: boolean}>({});
100
  const [addingPractice, setAddingPractice] = useState<boolean>(false);
101
+ const [addingImage, setAddingImage] = useState<boolean>(false);
102
  const [editForm, setEditForm] = useState<{
103
  content: string;
104
  translationBrief: string;
 
110
  imageUrl: '',
111
  imageAlt: ''
112
  });
113
+ const [imageForm, setImageForm] = useState<{
114
+ imageUrl: string;
115
+ imageAlt: string;
116
+ imageSize: number;
117
+ imageAlignment: 'left' | 'center' | 'right';
118
+ }>({
119
+ imageUrl: '',
120
+ imageAlt: '',
121
+ imageSize: 200,
122
+ imageAlignment: 'center'
123
+ });
124
  const [saving, setSaving] = useState(false);
125
  const [uploading, setUploading] = useState(false);
126
  const navigate = useNavigate();
 
1062
  });
1063
  };
1064
 
1065
+ const startAddingImage = () => {
1066
+ setAddingImage(true);
1067
+ setImageForm({
1068
+ imageUrl: '',
1069
+ imageAlt: '',
1070
+ imageSize: 200,
1071
+ imageAlignment: 'center'
1072
+ });
1073
+ };
1074
+
1075
+ const cancelAddingImage = () => {
1076
+ setAddingImage(false);
1077
+ setImageForm({
1078
+ imageUrl: '',
1079
+ imageAlt: '',
1080
+ imageSize: 200,
1081
+ imageAlignment: 'center'
1082
+ });
1083
+ };
1084
+
1085
  const saveNewPractice = async () => {
1086
  try {
1087
  setSaving(true);
 
1135
  }
1136
  };
1137
 
1138
+ const saveNewImage = async () => {
1139
+ try {
1140
+ setSaving(true);
1141
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
1142
+
1143
+ // Check if user is admin
1144
+ if (user.role !== 'admin') {
1145
+ return;
1146
+ }
1147
+
1148
+ if (!imageForm.imageUrl.trim()) {
1149
+ return;
1150
+ }
1151
+
1152
+ const payload = {
1153
+ title: `Week ${selectedWeek} Image Practice`,
1154
+ content: 'Image-based practice',
1155
+ sourceLanguage: 'English',
1156
+ weekNumber: selectedWeek,
1157
+ category: 'weekly-practice',
1158
+ imageUrl: imageForm.imageUrl.trim(),
1159
+ imageAlt: imageForm.imageAlt.trim() || null,
1160
+ imageSize: imageForm.imageSize,
1161
+ imageAlignment: imageForm.imageAlignment
1162
+ };
1163
+
1164
+ console.log('Saving new image with payload:', payload);
1165
+
1166
+ const response = await api.post('/api/auth/admin/weekly-practice', payload);
1167
+
1168
+ if (response.data) {
1169
+ console.log('Image saved successfully:', response.data);
1170
+ await fetchWeeklyPractice(false);
1171
+ setAddingImage(false);
1172
+ } else {
1173
+ console.error('Failed to save image');
1174
+ }
1175
+ } catch (error) {
1176
+ console.error('Failed to add image practice:', error);
1177
+ } finally {
1178
+ setSaving(false);
1179
+ }
1180
+ };
1181
+
1182
  const deletePractice = async (practiceId: string) => {
1183
  const user = JSON.parse(localStorage.getItem('user') || '{}');
1184
 
 
1752
  )}
1753
  </div>
1754
  </div>
1755
+ ) : addingImage ? (
1756
+ <div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6 w-full">
1757
+ <div className="flex items-center justify-between mb-4">
1758
+ <div className="flex items-center space-x-2">
1759
+ <div className="bg-blue-100 rounded-full p-2">
1760
+ <PlusIcon className="h-5 w-5 text-blue-600" />
1761
+ </div>
1762
+ <h3 className="text-lg font-semibold text-gray-900">Add New Image</h3>
1763
+ </div>
1764
+ <div className="flex items-center space-x-2">
1765
+ <button
1766
+ onClick={saveNewImage}
1767
+ disabled={saving}
1768
+ className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
1769
+ >
1770
+ {saving ? (
1771
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
1772
+ ) : (
1773
+ <>
1774
+ <CheckIcon className="h-4 w-4" />
1775
+ <span>Save Image</span>
1776
+ </>
1777
+ )}
1778
+ </button>
1779
+ <button
1780
+ onClick={cancelAddingImage}
1781
+ className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center space-x-2"
1782
+ >
1783
+ <XMarkIcon className="h-4 w-4" />
1784
+ <span>Cancel</span>
1785
+ </button>
1786
+ </div>
1787
+ </div>
1788
+ <div className="space-y-4">
1789
+ <div>
1790
+ <label className="block text-sm font-medium text-gray-700 mb-1">Image URL</label>
1791
+ <input
1792
+ type="text"
1793
+ value={imageForm.imageUrl}
1794
+ onChange={(e) => setImageForm({...imageForm, imageUrl: e.target.value})}
1795
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1796
+ placeholder="Enter image URL..."
1797
+ />
1798
+ </div>
1799
+ <div>
1800
+ <label className="block text-sm font-medium text-gray-700 mb-1">Image Alt Text</label>
1801
+ <input
1802
+ type="text"
1803
+ value={imageForm.imageAlt}
1804
+ onChange={(e) => setImageForm({...imageForm, imageAlt: e.target.value})}
1805
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1806
+ placeholder="Enter alt text for accessibility..."
1807
+ />
1808
+ </div>
1809
+ <div>
1810
+ <label className="block text-sm font-medium text-gray-700 mb-1">Upload Local Image</label>
1811
+ <input
1812
+ type="file"
1813
+ accept="image/*"
1814
+ onChange={async (e) => {
1815
+ const file = e.target.files?.[0];
1816
+ if (file) {
1817
+ try {
1818
+ const imageUrl = await handleFileUpload(file);
1819
+ setImageForm({ ...imageForm, imageUrl });
1820
+ } catch (error) {
1821
+ console.error('Error uploading file:', error);
1822
+ }
1823
+ }
1824
+ }}
1825
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1826
+ />
1827
+ {uploading && (
1828
+ <div className="mt-2 text-sm text-blue-600">
1829
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 inline-block mr-2"></div>
1830
+ Uploading...
1831
+ </div>
1832
+ )}
1833
+ </div>
1834
+ <div className="grid grid-cols-2 gap-4">
1835
+ <div>
1836
+ <label className="block text-sm font-medium text-gray-700 mb-1">Image Size</label>
1837
+ <select
1838
+ value={imageForm.imageSize}
1839
+ onChange={(e) => setImageForm({...imageForm, imageSize: parseInt(e.target.value)})}
1840
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1841
+ >
1842
+ <option value={150}>150px</option>
1843
+ <option value={200}>200px</option>
1844
+ <option value={300}>300px</option>
1845
+ </select>
1846
+ </div>
1847
+ <div>
1848
+ <label className="block text-sm font-medium text-gray-700 mb-1">Alignment</label>
1849
+ <select
1850
+ value={imageForm.imageAlignment}
1851
+ onChange={(e) => setImageForm({...imageForm, imageAlignment: e.target.value as 'left' | 'center' | 'right'})}
1852
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
1853
+ >
1854
+ <option value="left">Left</option>
1855
+ <option value="center">Center</option>
1856
+ <option value="right">Right</option>
1857
+ </select>
1858
+ </div>
1859
+ </div>
1860
+ </div>
1861
+ </div>
1862
  ) : (
1863
  <div className="bg-gradient-to-r from-orange-50 to-amber-50 rounded-xl p-6 border border-orange-200 border-dashed">
1864
  <div className="flex items-center justify-between">
 
1871
  <p className="text-orange-700 text-sm">Create a new practice example for Week {selectedWeek}</p>
1872
  </div>
1873
  </div>
1874
+ <div className="flex space-x-3">
1875
+ <button
1876
+ onClick={startAddingPractice}
1877
+ className="bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl"
1878
+ >
1879
+ <PlusIcon className="h-5 w-5" />
1880
+ <span className="font-medium">Add Practice</span>
1881
+ </button>
1882
+ {selectedWeek >= 3 && (
1883
+ <button
1884
+ onClick={startAddingImage}
1885
+ className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg transition-colors duration-200 flex items-center space-x-2 shadow-lg hover:shadow-xl"
1886
+ >
1887
+ <PlusIcon className="h-5 w-5" />
1888
+ <span className="font-medium">Add Image</span>
1889
+ </button>
1890
+ )}
1891
+ </div>
1892
  </div>
1893
  </div>
1894
  )}
 
2026
  ) : (
2027
  <div>
2028
  {practice.imageUrl && selectedWeek >= 3 ? (
2029
+ <div className={`flex flex-col md:flex-row gap-6 items-start ${
2030
+ practice.imageAlignment === 'left' ? 'md:flex-row' :
2031
+ practice.imageAlignment === 'right' ? 'md:flex-row-reverse' :
2032
+ 'md:flex-col'
2033
+ }`}>
2034
+ {/* Image with dynamic alignment */}
2035
+ <div className={`${
2036
+ practice.imageAlignment === 'left' ? 'w-full md:w-1/2' :
2037
+ practice.imageAlignment === 'right' ? 'w-full md:w-1/2' :
2038
+ 'w-full'
2039
+ } flex ${
2040
+ practice.imageAlignment === 'left' ? 'justify-start' :
2041
+ practice.imageAlignment === 'right' ? 'justify-end' :
2042
+ 'justify-center'
2043
+ }`}>
2044
  {practice.imageUrl.startsWith('data:') ? (
2045
  <div className="inline-block rounded-lg shadow-md overflow-hidden">
2046
  <img
2047
  src={practice.imageUrl}
2048
  alt={practice.imageAlt || 'Uploaded image'}
2049
  className="w-full h-auto"
2050
+ style={{
2051
+ height: `${practice.imageSize || 200}px`,
2052
+ width: 'auto',
2053
+ objectFit: 'contain'
2054
+ }}
2055
  onError={(e) => {
2056
  console.error('Error loading image:', e);
2057
  e.currentTarget.style.display = 'none';
 
2072
  </div>
2073
  )}
2074
  </div>
2075
+ {/* Text with conditional display */}
2076
+ {practice.content && practice.content !== 'Image-based practice' && (
2077
+ <div className={`${
2078
+ practice.imageAlignment === 'left' ? 'w-full md:w-1/2' :
2079
+ practice.imageAlignment === 'right' ? 'w-full md:w-1/2' :
2080
+ 'w-full'
2081
+ }`}>
2082
+ <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
2083
+ </div>
2084
+ )}
2085
  </div>
2086
  ) : (
2087
  <p className="text-orange-800 leading-relaxed text-lg font-source-text whitespace-pre-wrap">{practice.content}</p>
server/models/SourceText.js CHANGED
@@ -28,6 +28,8 @@ const sourceTextSchema = new mongoose.Schema({
28
  translationBrief: { type: String },
29
  imageUrl: { type: String },
30
  imageAlt: { type: String },
 
 
31
  culturalElements: [culturalElementSchema],
32
  difficulty: {
33
  type: String,
 
28
  translationBrief: { type: String },
29
  imageUrl: { type: String },
30
  imageAlt: { type: String },
31
+ imageSize: { type: Number, default: 200 },
32
+ imageAlignment: { type: String, enum: ['left', 'center', 'right'], default: 'center' },
33
  culturalElements: [culturalElementSchema],
34
  difficulty: {
35
  type: String,
server/routes/auth.js CHANGED
@@ -496,7 +496,9 @@ router.post('/admin/weekly-practice', authenticateToken, async (req, res) => {
496
  culturalElements: culturalElements || [],
497
  sourceType: 'weekly-practice',
498
  imageUrl: req.body.imageUrl || null,
499
- imageAlt: req.body.imageAlt || null
 
 
500
  });
501
 
502
  const savedPractice = await newWeeklyPractice.save();
 
496
  culturalElements: culturalElements || [],
497
  sourceType: 'weekly-practice',
498
  imageUrl: req.body.imageUrl || null,
499
+ imageAlt: req.body.imageAlt || null,
500
+ imageSize: req.body.imageSize || 200,
501
+ imageAlignment: req.body.imageAlignment || 'center'
502
  });
503
 
504
  const savedPractice = await newWeeklyPractice.save();