ChandimaPrabath commited on
Commit
0527ba9
1 Parent(s): 804544a
frontend/package-lock.json CHANGED
@@ -8,6 +8,11 @@
8
  "name": "frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
 
 
 
 
 
11
  "next": "14.2.5",
12
  "react": "^18",
13
  "react-dom": "^18"
@@ -87,6 +92,59 @@
87
  "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
88
  }
89
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  "node_modules/@humanwhocodes/config-array": {
91
  "version": "0.11.14",
92
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -833,6 +891,11 @@
833
  "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
834
  "dev": true
835
  },
 
 
 
 
 
836
  "node_modules/available-typed-arrays": {
837
  "version": "1.0.7",
838
  "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -857,6 +920,16 @@
857
  "node": ">=4"
858
  }
859
  },
 
 
 
 
 
 
 
 
 
 
860
  "node_modules/axobject-query": {
861
  "version": "3.1.1",
862
  "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -1048,6 +1121,17 @@
1048
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1049
  "dev": true
1050
  },
 
 
 
 
 
 
 
 
 
 
 
1051
  "node_modules/commander": {
1052
  "version": "4.1.1",
1053
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1235,6 +1319,14 @@
1235
  "url": "https://github.com/sponsors/ljharb"
1236
  }
1237
  },
 
 
 
 
 
 
 
 
1238
  "node_modules/didyoumean": {
1239
  "version": "1.2.2",
1240
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2013,6 +2105,25 @@
2013
  "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
2014
  "dev": true
2015
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2016
  "node_modules/for-each": {
2017
  "version": "0.3.3",
2018
  "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -2038,6 +2149,19 @@
2038
  "url": "https://github.com/sponsors/isaacs"
2039
  }
2040
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2041
  "node_modules/fs.realpath": {
2042
  "version": "1.0.0",
2043
  "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3035,6 +3159,25 @@
3035
  "node": ">=8.6"
3036
  }
3037
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3038
  "node_modules/minimatch": {
3039
  "version": "3.1.2",
3040
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3194,7 +3337,6 @@
3194
  "version": "4.1.1",
3195
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
3196
  "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
3197
- "dev": true,
3198
  "engines": {
3199
  "node": ">=0.10.0"
3200
  }
@@ -3664,13 +3806,17 @@
3664
  "version": "15.8.1",
3665
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
3666
  "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
3667
- "dev": true,
3668
  "dependencies": {
3669
  "loose-envify": "^1.4.0",
3670
  "object-assign": "^4.1.1",
3671
  "react-is": "^16.13.1"
3672
  }
3673
  },
 
 
 
 
 
3674
  "node_modules/punycode": {
3675
  "version": "2.3.1",
3676
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3726,8 +3872,7 @@
3726
  "node_modules/react-is": {
3727
  "version": "16.13.1",
3728
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
3729
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
3730
- "dev": true
3731
  },
3732
  "node_modules/read-cache": {
3733
  "version": "1.0.0",
 
8
  "name": "frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
12
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
13
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
14
+ "@fortawesome/react-fontawesome": "^0.2.2",
15
+ "axios": "^1.7.4",
16
  "next": "14.2.5",
17
  "react": "^18",
18
  "react-dom": "^18"
 
92
  "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
93
  }
94
  },
95
+ "node_modules/@fortawesome/fontawesome-common-types": {
96
+ "version": "6.6.0",
97
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
98
+ "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
99
+ "engines": {
100
+ "node": ">=6"
101
+ }
102
+ },
103
+ "node_modules/@fortawesome/fontawesome-svg-core": {
104
+ "version": "6.6.0",
105
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
106
+ "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
107
+ "dependencies": {
108
+ "@fortawesome/fontawesome-common-types": "6.6.0"
109
+ },
110
+ "engines": {
111
+ "node": ">=6"
112
+ }
113
+ },
114
+ "node_modules/@fortawesome/free-regular-svg-icons": {
115
+ "version": "6.6.0",
116
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
117
+ "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
118
+ "dependencies": {
119
+ "@fortawesome/fontawesome-common-types": "6.6.0"
120
+ },
121
+ "engines": {
122
+ "node": ">=6"
123
+ }
124
+ },
125
+ "node_modules/@fortawesome/free-solid-svg-icons": {
126
+ "version": "6.6.0",
127
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
128
+ "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
129
+ "dependencies": {
130
+ "@fortawesome/fontawesome-common-types": "6.6.0"
131
+ },
132
+ "engines": {
133
+ "node": ">=6"
134
+ }
135
+ },
136
+ "node_modules/@fortawesome/react-fontawesome": {
137
+ "version": "0.2.2",
138
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
139
+ "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
140
+ "dependencies": {
141
+ "prop-types": "^15.8.1"
142
+ },
143
+ "peerDependencies": {
144
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
145
+ "react": ">=16.3"
146
+ }
147
+ },
148
  "node_modules/@humanwhocodes/config-array": {
149
  "version": "0.11.14",
150
  "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
 
891
  "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
892
  "dev": true
893
  },
894
+ "node_modules/asynckit": {
895
+ "version": "0.4.0",
896
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
897
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
898
+ },
899
  "node_modules/available-typed-arrays": {
900
  "version": "1.0.7",
901
  "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
 
920
  "node": ">=4"
921
  }
922
  },
923
+ "node_modules/axios": {
924
+ "version": "1.7.4",
925
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
926
+ "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
927
+ "dependencies": {
928
+ "follow-redirects": "^1.15.6",
929
+ "form-data": "^4.0.0",
930
+ "proxy-from-env": "^1.1.0"
931
+ }
932
+ },
933
  "node_modules/axobject-query": {
934
  "version": "3.1.1",
935
  "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
 
1121
  "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1122
  "dev": true
1123
  },
1124
+ "node_modules/combined-stream": {
1125
+ "version": "1.0.8",
1126
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1127
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1128
+ "dependencies": {
1129
+ "delayed-stream": "~1.0.0"
1130
+ },
1131
+ "engines": {
1132
+ "node": ">= 0.8"
1133
+ }
1134
+ },
1135
  "node_modules/commander": {
1136
  "version": "4.1.1",
1137
  "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
 
1319
  "url": "https://github.com/sponsors/ljharb"
1320
  }
1321
  },
1322
+ "node_modules/delayed-stream": {
1323
+ "version": "1.0.0",
1324
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1325
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1326
+ "engines": {
1327
+ "node": ">=0.4.0"
1328
+ }
1329
+ },
1330
  "node_modules/didyoumean": {
1331
  "version": "1.2.2",
1332
  "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
 
2105
  "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
2106
  "dev": true
2107
  },
2108
+ "node_modules/follow-redirects": {
2109
+ "version": "1.15.6",
2110
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
2111
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
2112
+ "funding": [
2113
+ {
2114
+ "type": "individual",
2115
+ "url": "https://github.com/sponsors/RubenVerborgh"
2116
+ }
2117
+ ],
2118
+ "engines": {
2119
+ "node": ">=4.0"
2120
+ },
2121
+ "peerDependenciesMeta": {
2122
+ "debug": {
2123
+ "optional": true
2124
+ }
2125
+ }
2126
+ },
2127
  "node_modules/for-each": {
2128
  "version": "0.3.3",
2129
  "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
 
2149
  "url": "https://github.com/sponsors/isaacs"
2150
  }
2151
  },
2152
+ "node_modules/form-data": {
2153
+ "version": "4.0.0",
2154
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
2155
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
2156
+ "dependencies": {
2157
+ "asynckit": "^0.4.0",
2158
+ "combined-stream": "^1.0.8",
2159
+ "mime-types": "^2.1.12"
2160
+ },
2161
+ "engines": {
2162
+ "node": ">= 6"
2163
+ }
2164
+ },
2165
  "node_modules/fs.realpath": {
2166
  "version": "1.0.0",
2167
  "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 
3159
  "node": ">=8.6"
3160
  }
3161
  },
3162
+ "node_modules/mime-db": {
3163
+ "version": "1.52.0",
3164
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
3165
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
3166
+ "engines": {
3167
+ "node": ">= 0.6"
3168
+ }
3169
+ },
3170
+ "node_modules/mime-types": {
3171
+ "version": "2.1.35",
3172
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
3173
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
3174
+ "dependencies": {
3175
+ "mime-db": "1.52.0"
3176
+ },
3177
+ "engines": {
3178
+ "node": ">= 0.6"
3179
+ }
3180
+ },
3181
  "node_modules/minimatch": {
3182
  "version": "3.1.2",
3183
  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
 
3337
  "version": "4.1.1",
3338
  "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
3339
  "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
 
3340
  "engines": {
3341
  "node": ">=0.10.0"
3342
  }
 
3806
  "version": "15.8.1",
3807
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
3808
  "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
 
3809
  "dependencies": {
3810
  "loose-envify": "^1.4.0",
3811
  "object-assign": "^4.1.1",
3812
  "react-is": "^16.13.1"
3813
  }
3814
  },
3815
+ "node_modules/proxy-from-env": {
3816
+ "version": "1.1.0",
3817
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
3818
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
3819
+ },
3820
  "node_modules/punycode": {
3821
  "version": "2.3.1",
3822
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
 
3872
  "node_modules/react-is": {
3873
  "version": "16.13.1",
3874
  "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
3875
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
 
3876
  },
3877
  "node_modules/read-cache": {
3878
  "version": "1.0.0",
frontend/package.json CHANGED
@@ -9,14 +9,19 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
 
 
 
 
 
 
12
  "react": "^18",
13
- "react-dom": "^18",
14
- "next": "14.2.5"
15
  },
16
  "devDependencies": {
17
- "postcss": "^8",
18
- "tailwindcss": "^3.4.1",
19
  "eslint": "^8",
20
- "eslint-config-next": "14.2.5"
 
 
21
  }
22
  }
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
13
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
14
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
15
+ "@fortawesome/react-fontawesome": "^0.2.2",
16
+ "axios": "^1.7.4",
17
+ "next": "14.2.5",
18
  "react": "^18",
19
+ "react-dom": "^18"
 
20
  },
21
  "devDependencies": {
 
 
22
  "eslint": "^8",
23
+ "eslint-config-next": "14.2.5",
24
+ "postcss": "^8",
25
+ "tailwindcss": "^3.4.1"
26
  }
27
  }
frontend/src/api/apiClient.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { LoadBalancerAPI } from '@/lib/LoadBalancer';
2
+ import config from '@/lib/config';
3
+
4
+ const apiClient = new LoadBalancerAPI(config.apiBaseUrl);
5
+
6
+ export default apiClient;
frontend/src/api/searchApi.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // lib/searchApi.js or utils/searchApi.js based on your structure
2
+ import axios from 'axios';
3
+ import config from '@/lib/config'; // Adjust path if necessary
4
+
5
+ const searchUrl = config.searchUrl;
6
+
7
+ /**
8
+ * Perform a search query.
9
+ * @param {string} query - The search query.
10
+ * @returns {Promise<Object>} - The search results.
11
+ */
12
+ export const search = async (query) => {
13
+ try {
14
+ const response = await axios.post(searchUrl, { query });
15
+ console.log(response);
16
+ return response.data;
17
+ } catch (error) {
18
+ console.error("Error performing search:", error);
19
+ throw error; // Rethrow the error to be handled by the caller
20
+ }
21
+ };
frontend/src/app/favicon.ico DELETED
Binary file (25.9 kB)
 
frontend/src/app/films/filmsPage.css ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .films-page-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ padding: 20px;
6
+ max-width: 1300px;
7
+ margin: 0 auto;
8
+ }
9
+
10
+ .films-page {
11
+ display: grid;
12
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
13
+ gap: 50px;
14
+ justify-items: center;
15
+ align-items: start;
16
+ width: 100%;
17
+ }
18
+
19
+ /* Media query for smaller screens */
20
+ @media (max-width: 768px) {
21
+ .films-page {
22
+ grid-template-columns: repeat(auto-fit, minmax(150px, .1fr));
23
+ gap: 10px;
24
+ }
25
+ }
26
+
27
+ @media (max-width: 480px) {
28
+ .films-page {
29
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
30
+ }
31
+ }
32
+
33
+ .pagination-controls {
34
+ margin-top: 20px;
35
+ display: flex;
36
+ align-items: center;
37
+ }
38
+
39
+ .pagination-button {
40
+ background-color: #2c2b2b;
41
+ color: #f5f5f5;
42
+ border: none;
43
+ border-radius: 5px;
44
+ padding: 5px;
45
+ width: 50px;
46
+ text-align: center;
47
+ margin: 0 10px;
48
+ cursor: pointer;
49
+ transition: background-color 0.3s ease, transform 0.3s ease;
50
+ }
51
+
52
+ .pagination-button:hover {
53
+ background-color: #555;
54
+ transform: scale(1.05);
55
+ }
56
+
57
+ .pagination-button:disabled {
58
+ background-color: #555;
59
+ cursor: not-allowed;
60
+ }
61
+
62
+ .page-info {
63
+ font-size: 1.2em;
64
+ color: #f5f5f5;
65
+ }
66
+
67
+ /* Handle animations on page load */
68
+ @keyframes pageLoad {
69
+ from {
70
+ opacity: 0;
71
+ }
72
+ to {
73
+ opacity: 1;
74
+ }
75
+ }
76
+ .films-page-container {
77
+ animation: pageLoad 1s ease;
78
+ }
frontend/src/app/films/page.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
+ import apiClient from '@/api/apiClient'; // Updated for Next.js absolute import
5
+ import FilmCard from '@/components/film/card'; // Updated for Next.js absolute import
6
+ import { useFilmContext } from '@/context/FilmContext'; // Updated for Next.js absolute import
7
+ import './filmsPage.css'; // Updated for Next.js absolute import
8
+
9
+ import { faCaretLeft } from '@fortawesome/free-solid-svg-icons';
10
+ import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
11
+
12
+ const FILMS_PER_PAGE = 2;
13
+
14
+ export default function FilmsPage() {
15
+ const { films, setFilms } = useFilmContext();
16
+ const [currentPage, setCurrentPage] = useState(1);
17
+
18
+ useEffect(() => {
19
+ if (films.length === 0) {
20
+ apiClient.getAllFilms()
21
+ .then(response => {
22
+ console.log('All films:', response);
23
+ setFilms(response.map(film => film.replace('films/', '')));
24
+ })
25
+ .catch(error => {
26
+ console.error('Failed to get all films:', error);
27
+ });
28
+ }
29
+ }, [films, setFilms]);
30
+
31
+ const startIndex = (currentPage - 1) * FILMS_PER_PAGE;
32
+ const currentFilms = films.slice(startIndex, startIndex + FILMS_PER_PAGE);
33
+
34
+ const handleNextPage = () => {
35
+ setCurrentPage(prevPage => prevPage + 1);
36
+ };
37
+
38
+ const handlePrevPage = () => {
39
+ setCurrentPage(prevPage => Math.max(prevPage - 1, 1));
40
+ };
41
+
42
+ // Calculate total number of pages
43
+ const totalPages = Math.ceil(films.length / FILMS_PER_PAGE);
44
+
45
+ // Determine if the previous and next buttons should be enabled
46
+ const isPrevButtonEnabled = currentPage > 1;
47
+ const isNextButtonEnabled = currentPage < totalPages;
48
+
49
+ return (
50
+ <div className="films-page-container">
51
+ <div className="pagination-controls">
52
+ <button
53
+ onClick={handlePrevPage}
54
+ disabled={!isPrevButtonEnabled}
55
+ className="pagination-button"
56
+ >
57
+ <FontAwesomeIcon
58
+ icon={faCaretLeft}
59
+ size="2xl"
60
+ color='#e88f36'
61
+ bounce={isPrevButtonEnabled}
62
+ />
63
+ </button>
64
+ <span className="page-info">
65
+ {currentPage} - {totalPages}
66
+ </span>
67
+ <button
68
+ onClick={handleNextPage}
69
+ disabled={!isNextButtonEnabled}
70
+ className="pagination-button"
71
+ >
72
+ <FontAwesomeIcon
73
+ icon={faCaretRight}
74
+ size="2xl"
75
+ color='#e88f36'
76
+ bounce={isNextButtonEnabled}
77
+ />
78
+ </button>
79
+ </div>
80
+ <div className="films-page">
81
+ {currentFilms.map(title => (
82
+ <FilmCard key={title} title={title} />
83
+ ))}
84
+ </div>
85
+ </div>
86
+ );
87
+ }
frontend/src/app/layout.js CHANGED
@@ -1,17 +1,40 @@
1
  import { Inter } from "next/font/google";
2
  import "./globals.css";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  const inter = Inter({ subsets: ["latin"] });
5
 
6
  export const metadata = {
7
- title: "Create Next App",
8
  description: "Generated by create next app",
9
  };
10
 
11
  export default function RootLayout({ children }) {
12
  return (
13
  <html lang="en">
14
- <body className={inter.className}>{children}</body>
 
 
 
 
 
15
  </html>
16
  );
17
  }
 
1
  import { Inter } from "next/font/google";
2
  import "./globals.css";
3
+ import Navbar from "../components/Navbar";
4
+ import { FilmProvider } from "@/context/FilmContext";
5
+ import { library } from "@fortawesome/fontawesome-svg-core";
6
+ import { config } from "@fortawesome/fontawesome-svg-core";
7
+ import "@fortawesome/fontawesome-svg-core/styles.css";
8
+ config.autoAddCss = false;
9
+ import {
10
+ faEllipsisVertical,
11
+ faPlay,
12
+ faChevronLeft,
13
+ faCaretDown,
14
+ faCaretLeft,
15
+ faCaretRight,
16
+ faMagnifyingGlass,
17
+ faHome,
18
+ faFilm,
19
+ faTv,
20
+ } from "@fortawesome/free-solid-svg-icons";
21
 
22
  const inter = Inter({ subsets: ["latin"] });
23
 
24
  export const metadata = {
25
+ title: "Home",
26
  description: "Generated by create next app",
27
  };
28
 
29
  export default function RootLayout({ children }) {
30
  return (
31
  <html lang="en">
32
+ <body className={inter.className}>
33
+ <header>
34
+ <Navbar />
35
+ </header>
36
+ <FilmProvider>{children}</FilmProvider>
37
+ </body>
38
  </html>
39
  );
40
  }
frontend/src/app/page.js CHANGED
@@ -1,113 +1,43 @@
1
- import Image from "next/image";
 
 
2
 
3
  export default function Home() {
4
- return (
5
- <main className="flex min-h-screen flex-col items-center justify-between p-24">
6
- <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
7
- <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
8
- Get started by editing&nbsp;
9
- <code className="font-mono font-bold">src/app/page.js</code>
10
- </p>
11
- <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
12
- <a
13
- className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
14
- href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
15
- target="_blank"
16
- rel="noopener noreferrer"
17
- >
18
- By{" "}
19
- <Image
20
- src="/vercel.svg"
21
- alt="Vercel Logo"
22
- className="dark:invert"
23
- width={100}
24
- height={24}
25
- priority
26
- />
27
- </a>
28
- </div>
29
- </div>
30
 
31
- <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-full sm:before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full sm:after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
32
- <Image
33
- className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
34
- src="/next.svg"
35
- alt="Next.js Logo"
36
- width={180}
37
- height={37}
38
- priority
39
- />
40
- </div>
 
 
 
41
 
42
- <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
43
- <a
44
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
45
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
46
- target="_blank"
47
- rel="noopener noreferrer"
48
- >
49
- <h2 className={`mb-3 text-2xl font-semibold`}>
50
- Docs{" "}
51
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
52
- -&gt;
53
- </span>
54
- </h2>
55
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
56
- Find in-depth information about Next.js features and API.
57
- </p>
58
- </a>
59
 
60
- <a
61
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
62
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800 hover:dark:bg-opacity-30"
63
- target="_blank"
64
- rel="noopener noreferrer"
65
- >
66
- <h2 className={`mb-3 text-2xl font-semibold`}>
67
- Learn{" "}
68
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
69
- -&gt;
70
- </span>
71
- </h2>
72
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
73
- Learn about Next.js in an interactive course with&nbsp;quizzes!
74
- </p>
75
- </a>
76
 
77
- <a
78
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
79
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
80
- target="_blank"
81
- rel="noopener noreferrer"
82
- >
83
- <h2 className={`mb-3 text-2xl font-semibold`}>
84
- Templates{" "}
85
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
86
- -&gt;
87
- </span>
88
- </h2>
89
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
90
- Explore starter templates for Next.js.
91
- </p>
92
- </a>
93
-
94
- <a
95
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
96
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
97
- target="_blank"
98
- rel="noopener noreferrer"
99
- >
100
- <h2 className={`mb-3 text-2xl font-semibold`}>
101
- Deploy{" "}
102
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
103
- -&gt;
104
- </span>
105
- </h2>
106
- <p className={`m-0 max-w-[30ch] text-sm opacity-50 text-balance`}>
107
- Instantly deploy your Next.js site to a shareable URL with Vercel.
108
- </p>
109
- </a>
110
  </div>
111
- </main>
112
  );
113
  }
 
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import apiClient from "@/api/apiClient";
4
 
5
  export default function Home() {
6
+ const [films, setFilms] = useState([]);
7
+ const [loading, setLoading] = useState(true);
8
+ const [error, setError] = useState(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ useEffect(() => {
11
+ async function fetchFilms() {
12
+ try {
13
+ const all_films = await apiClient.getAllFilms();
14
+ setFilms(all_films);
15
+ console.log(all_films);
16
+ } catch (error) {
17
+ setError('Failed to load films.');
18
+ console.error('Error fetching films:', error);
19
+ } finally {
20
+ setLoading(false);
21
+ }
22
+ }
23
 
24
+ fetchFilms();
25
+ }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ if (loading) return <p>Loading...</p>;
28
+ if (error) return <p>{error}</p>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ return (
31
+ <>
32
+ <div>
33
+ <h1>Welcome to the Streaming Service</h1>
34
+ <p>Explore our collection of films and TV shows.</p>
35
+ <ul>
36
+ {films.map((film, index) => (
37
+ <li key={index}>{film.title}</li>
38
+ ))}
39
+ </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </div>
41
+ </>
42
  );
43
  }
frontend/src/app/player/film/[title]/page.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useParams } from 'next/navigation';
4
+
5
+ export default function FilmPlayer() {
6
+ const { title } = useParams();
7
+
8
+ return (
9
+ <>
10
+ <div>
11
+ <h1 className='text-white'>Film Player - {title}</h1>
12
+ {/* Add film player component here */}
13
+ </div>
14
+ </>
15
+ );
16
+ }
frontend/src/app/player/tvshow/[title]/[season]/[episode]/page.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useParams } from "next/navigation";
4
+
5
+ export default function TVShowPlayer() {
6
+ const { title, season, episode } = useParams();
7
+
8
+ return (
9
+ <>
10
+ <div>
11
+ <h1 className="text-white">TV Show Player - {title + season + episode}</h1>
12
+ {/* Add TV show player component here */}
13
+ </div>
14
+ </>
15
+ );
16
+ }
frontend/src/app/search/page.js ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"; // Indicate that this file is a client component in Next.js
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { search } from "@/api/searchApi"; // Ensure the path is correct for your API
6
+ import "./searchPage.css";
7
+
8
+ const SearchPage = () => {
9
+ const [query, setQuery] = useState("");
10
+ const [results, setResults] = useState({ films: [], tv_series: [] });
11
+ const [loading, setLoading] = useState(false);
12
+ const [error, setError] = useState(null);
13
+ const [debouncedQuery, setDebouncedQuery] = useState("");
14
+
15
+ const router = useRouter(); // Use Next.js useRouter for navigation
16
+
17
+ useEffect(() => {
18
+ const handler = setTimeout(() => {
19
+ if (debouncedQuery) {
20
+ handleSearch();
21
+ }
22
+ }, 200); // Delay of 200ms
23
+
24
+ return () => {
25
+ clearTimeout(handler);
26
+ };
27
+ }, [debouncedQuery]);
28
+
29
+ const handleSearch = async () => {
30
+ setLoading(true);
31
+ setError(null);
32
+ try {
33
+ const data = await search(debouncedQuery);
34
+ setResults(data);
35
+ } catch (err) {
36
+ setError("Failed to fetch search results.");
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ const handleChange = (e) => {
43
+ setQuery(e.target.value);
44
+ setDebouncedQuery(e.target.value);
45
+ };
46
+
47
+ const handleItemClick = (path) => {
48
+ router.push(path); // Use router.push for navigation
49
+ };
50
+
51
+ return (
52
+ <div className="search-page">
53
+ <div className="search-container">
54
+ <input
55
+ type="text"
56
+ value={query}
57
+ onChange={handleChange}
58
+ placeholder="Search for films or TV shows..."
59
+ className="search-input"
60
+ />
61
+ {loading && (
62
+ <div className="loading-indicator">
63
+ <div className="spinner"></div>
64
+ </div>
65
+ )}
66
+ </div>
67
+ {error && <div className="error-message">{error}</div>}
68
+ <div className="results-container">
69
+ {results.films.length > 0 && (
70
+ <div className="results-section">
71
+ <h2>Films</h2>
72
+ <ul className="results-list">
73
+ {results.films.map((film, index) => (
74
+ <li
75
+ key={index}
76
+ className="result-item"
77
+ onClick={() => handleItemClick(`/film/${encodeURIComponent(film)}`)}
78
+ >
79
+ {film}
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ </div>
84
+ )}
85
+ {results.tv_series.length > 0 && (
86
+ <div className="results-section">
87
+ <h2>TV Series</h2>
88
+ <ul className="results-list">
89
+ {results.tv_series.map((series, index) => (
90
+ <li
91
+ key={index}
92
+ className="result-item"
93
+ onClick={() => handleItemClick(`/tvshow/${encodeURIComponent(series)}`)}
94
+ >
95
+ {series}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ </div>
100
+ )}
101
+ </div>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default SearchPage;
frontend/src/app/search/searchPage.css ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .search-page {
2
+ background-color: #121212;
3
+ color: #ffffff;
4
+ min-height: 85vh;
5
+ transition: background-color 0.3s ease;
6
+ font-family: "Signika", sans-serif;
7
+ font-optical-sizing: auto;
8
+ font-style: normal;
9
+ font-variation-settings: "GRAD" 0;
10
+ }
11
+
12
+ /* Search container styles */
13
+ .search-container {
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ padding: 20px;
18
+ transition: transform 0.3s ease;
19
+ }
20
+
21
+ /* Input field styling with animation */
22
+ .search-input {
23
+ width: 100%;
24
+ max-width: 600px;
25
+ padding: 10px;
26
+ font-size: 16px;
27
+ border: 1px solid #444;
28
+ border-radius: 5px;
29
+ background-color: #1e1e1e;
30
+ color: #ffffff;
31
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
32
+ }
33
+
34
+ .search-input:focus {
35
+ outline: none;
36
+ border-color: #ff6f00;
37
+ box-shadow: 0 0 8px rgba(255, 111, 0, 0.6);
38
+ transform: scale(1.02);
39
+ }
40
+
41
+ /* Loading indicator animation */
42
+ .loading-indicator {
43
+ margin-top: 10px;
44
+ font-size: 18px;
45
+ color: #ff6f00;
46
+ opacity: 0;
47
+ animation: fadeIn 1s forwards;
48
+ }
49
+
50
+ @keyframes fadeIn {
51
+ from {
52
+ opacity: 0;
53
+ }
54
+ to {
55
+ opacity: 1;
56
+ }
57
+ }
58
+
59
+ /* Error message styling */
60
+ .error-message {
61
+ color: #ff6f00;
62
+ text-align: center;
63
+ margin-top: 20px;
64
+ animation: bounce 1s infinite;
65
+ }
66
+
67
+ @keyframes bounce {
68
+ 0%, 20%, 50%, 80%, 100% {
69
+ transform: translateY(0);
70
+ }
71
+ 40% {
72
+ transform: translateY(-10px);
73
+ }
74
+ 60% {
75
+ transform: translateY(-5px);
76
+ }
77
+ }
78
+
79
+ /* Results container styles */
80
+ .results-container {
81
+ padding: 20px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ }
86
+
87
+ /* Results section styles */
88
+ .results-section {
89
+ width: 100%;
90
+ max-width: 800px;
91
+ margin-bottom: 20px;
92
+ transition: opacity 0.3s ease;
93
+ }
94
+
95
+ .results-section h2 {
96
+ font-size: 24px;
97
+ border-bottom: 2px solid #444;
98
+ padding-bottom: 10px;
99
+ margin-bottom: 15px;
100
+ color: #ff6f00;
101
+ animation: fadeIn 1s ease;
102
+ }
103
+
104
+ /* Results list styles */
105
+ .results-list {
106
+ list-style-type: none;
107
+ padding: 0;
108
+ }
109
+
110
+ .result-item {
111
+ background-color: #1e1e1e;
112
+ border: 1px solid #444;
113
+ border-radius: 5px;
114
+ margin-bottom: 10px;
115
+ padding: 10px;
116
+ transition: background-color 0.3s ease, transform 0.3s ease;
117
+ animation: slideIn 0.5s ease;
118
+ }
119
+
120
+ @keyframes slideIn {
121
+ from {
122
+ transform: translateY(20px);
123
+ opacity: 0;
124
+ }
125
+ to {
126
+ transform: translateY(0);
127
+ opacity: 1;
128
+ }
129
+ }
130
+
131
+ .result-item:hover {
132
+ background-color: #333;
133
+ transform: scale(1.02);
134
+ }
135
+
136
+ /* Link styling */
137
+ .result-link {
138
+ text-decoration: none;
139
+ color: #ffffff;
140
+ font-size: 18px;
141
+ transition: color 0.3s ease;
142
+ }
143
+
144
+ .result-link:hover {
145
+ text-decoration: underline;
146
+ color: #ff6f00;
147
+ }
148
+
149
+ /* Handle animations on page load */
150
+ @keyframes pageLoad {
151
+ from {
152
+ opacity: 0;
153
+ }
154
+ to {
155
+ opacity: 1;
156
+ }
157
+ }
158
+ .search-page {
159
+ animation: pageLoad 1s ease;
160
+ }
161
+
162
+ /* Loading indicator styles */
163
+ .loading-indicator {
164
+ display: flex;
165
+ align-items: center;
166
+ margin-top: 20px;
167
+ font-size: 18px;
168
+ color: #ff6f00;
169
+ }
170
+
171
+ .loading-indicator .spinner {
172
+ width: 20px;
173
+ height: 20px;
174
+ border: 4px solid #ff6f00;
175
+ border-top: 4px solid transparent;
176
+ border-radius: 50%;
177
+ margin-right: 10px;
178
+ animation: spin 1s linear infinite;
179
+ }
180
+
181
+ /* Spinner animation */
182
+ @keyframes spin {
183
+ from {
184
+ transform: rotate(0deg);
185
+ }
186
+ to {
187
+ transform: rotate(360deg);
188
+ }
189
+ }
frontend/src/app/tvshows/page.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function TVShows() {
2
+ return (
3
+ <>
4
+ <div>
5
+ <h1>TV Shows</h1>
6
+ <p className='text-white'>Browse our TV show collection.</p>
7
+ {/* Add TV show list here */}
8
+ </div>
9
+ </>
10
+ );
11
+ }
frontend/src/components/Navbar.css ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Container for the navbar and underline */
2
+ nav {
3
+ position: relative;
4
+ }
5
+
6
+ .nav-links {
7
+ list-style: none;
8
+ padding: 0;
9
+ margin: 0;
10
+ display: flex;
11
+ justify-content: center;
12
+ position: relative; /* Ensure it's positioned relative for absolute positioning of underlines */
13
+ }
14
+
15
+ @import url('https://fonts.googleapis.com/css2?family=Pacifico&family=Rubik+Burned&family=Rubik+Marker+Hatch&family=Rubik+Maze&family=Rubik+Microbe&family=Rubik:ital,wght@0,300..900;1,300..900&display=swap');
16
+
17
+ .nav-link {
18
+ text-decoration: none;
19
+ color: #b0b0b0;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ width: 70px;
24
+ height: 50px;
25
+ position: relative;
26
+ font-size: 1em;
27
+ font-family: "Signika", sans-serif;
28
+ font-optical-sizing: auto;
29
+ font-style: normal;
30
+ font-variation-settings: "GRAD" 0;
31
+ font-weight: 600;
32
+ padding: 0 15px;
33
+ transition: color 0.3s ease, transform 0.3s ease;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ .nav-link-menue {
38
+ text-decoration: none;
39
+ color: #b0b0b0;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ position: relative;
44
+ padding: 11px 10px;
45
+ transition: color 0.3s ease, transform 0.3s ease;
46
+ box-sizing: border-box;
47
+ }
48
+
49
+ .nav-link-menue.active {
50
+ color: #ffffff;
51
+ }
52
+
53
+ .nav-link-menue:hover {
54
+ color: #ffffff;
55
+ transform: translateY(-2px);
56
+ }
57
+
58
+ .nav-link.active {
59
+ color: #ffffff;
60
+ }
61
+
62
+ .nav-link:hover {
63
+ color: #ffffff;
64
+ transform: translateY(-2px);
65
+ }
66
+
67
+ .underline {
68
+ position: absolute;
69
+ bottom: -3px; /* Adjust this to position the underline correctly at the bottom of the navbar */
70
+ height: 3px;
71
+ transition: width 0.3s ease, left 0.3s ease;
72
+ }
73
+
74
+ .active-underline {
75
+ background-color: #e88f36; /* Warm orange underline for active route */
76
+ }
77
+
78
+ .hover-underline {
79
+ background-color: #007bc7; /* Bright blue underline for hover effect */
80
+ }
81
+
82
+ /* Media Queries for Mobile Devices */
83
+ @media (max-width: 768px) {
84
+ .nav-link {
85
+ width: 70px;
86
+ padding: 10px;
87
+ }
88
+ }
89
+
frontend/src/components/Navbar.js ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import { usePathname } from "next/navigation";
4
+ import Link from "next/link";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { faHome, faMagnifyingGlass, faFilm, faTv, faCaretDown } from "@fortawesome/free-solid-svg-icons";
7
+ import "./Navbar.css"; // Ensure CSS is in the correct location
8
+
9
+ function Navbar() {
10
+ const pathname = usePathname(); // Use Next.js usePathname
11
+ const [hoveredLink, setHoveredLink] = useState(null);
12
+ const [prevLink, setPrevLink] = useState(null);
13
+ const [isModalOpen, setIsModalOpen] = useState(false);
14
+
15
+ const handleModalClose = () => {
16
+ setIsModalOpen(false);
17
+ };
18
+
19
+ const handleModalOpen = () => {
20
+ setIsModalOpen(!isModalOpen);
21
+ };
22
+
23
+ useEffect(() => {
24
+ const activeLink = document.querySelector(
25
+ `.nav-link[href="${pathname}"]`
26
+ );
27
+ if (activeLink) {
28
+ setPrevLink(activeLink);
29
+ }
30
+ }, [pathname]);
31
+
32
+ const handleMouseEnter = (e) => {
33
+ setPrevLink(document.querySelector(".nav-link.active"));
34
+ setHoveredLink(e.target);
35
+ };
36
+
37
+ const handleMouseLeave = () => {
38
+ setHoveredLink(null);
39
+ };
40
+
41
+ return (
42
+ <nav>
43
+ <ul className="nav-links">
44
+ <li>
45
+ <Link
46
+ href="/"
47
+ className={`nav-link ${pathname === "/" ? "active" : ""}`}
48
+ onMouseEnter={handleMouseEnter}
49
+ onMouseLeave={handleMouseLeave}
50
+ >
51
+ <FontAwesomeIcon icon={faHome} size="lg" />
52
+ </Link>
53
+ </li>
54
+ <li>
55
+ <Link
56
+ href="/search"
57
+ className={`nav-link ${pathname === "/search" ? "active" : ""}`}
58
+ onMouseEnter={handleMouseEnter}
59
+ onMouseLeave={handleMouseLeave}
60
+ >
61
+ <FontAwesomeIcon icon={faMagnifyingGlass} size="lg" />
62
+ </Link>
63
+ </li>
64
+ <li>
65
+ <Link
66
+ href="/films"
67
+ className={`nav-link ${pathname === "/films" ? "active" : ""}`}
68
+ onMouseEnter={handleMouseEnter}
69
+ onMouseLeave={handleMouseLeave}
70
+ >
71
+ <FontAwesomeIcon icon={faFilm} size="lg" />
72
+ </Link>
73
+ </li>
74
+ <li>
75
+ <Link
76
+ href="/tvshows"
77
+ className={`nav-link ${pathname === "/tvshows" ? "active" : ""}`}
78
+ onMouseEnter={handleMouseEnter}
79
+ onMouseLeave={handleMouseLeave}
80
+ >
81
+ <FontAwesomeIcon icon={faTv} size="lg" />
82
+ </Link>
83
+ </li>
84
+ <li>
85
+ <FontAwesomeIcon
86
+ className="nav-link-menue"
87
+ icon={faCaretDown}
88
+ size="xl"
89
+ onClick={handleModalOpen}
90
+ />
91
+ </li>
92
+ </ul>
93
+ <span
94
+ className="underline active-underline"
95
+ style={
96
+ prevLink
97
+ ? {
98
+ width: prevLink.offsetWidth,
99
+ left: prevLink.offsetLeft,
100
+ }
101
+ : {}
102
+ }
103
+ />
104
+ <span
105
+ className="underline hover-underline"
106
+ style={
107
+ hoveredLink
108
+ ? {
109
+ width: hoveredLink.offsetWidth,
110
+ left: hoveredLink.offsetLeft,
111
+ }
112
+ : {}
113
+ }
114
+ />
115
+ </nav>
116
+ );
117
+ }
118
+
119
+ export default Navbar;
frontend/src/components/film/card.css ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .film-card {
2
+ box-sizing: border-box;
3
+ width: 220px;
4
+ height: 390px;
5
+ border: 1px solid #333;
6
+ background-color: #2c2c2c;
7
+ border-radius: 15px;
8
+ overflow: hidden;
9
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
10
+ transition: background-color 0.3s ease;
11
+ display: flex;
12
+ flex-direction: column;
13
+ justify-content: space-between;
14
+ margin: 10px;
15
+ position: relative;
16
+ }
17
+
18
+ .film-card:hover {
19
+ background-color: #1e1e1e;
20
+ }
21
+
22
+ .film-card-image {
23
+ width: 100%;
24
+ object-fit: cover;
25
+ border-bottom: 3px solid #f8b525;
26
+ transition: transform 0.5s ease;
27
+ position: relative;
28
+ z-index: 1;
29
+ }
30
+
31
+ .film-card:hover .film-card-image {
32
+ transform: scale(1.1);
33
+ }
34
+
35
+ .film-card-overlay {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ background: linear-gradient(to bottom, rgba(18, 36, 65, 0) 0%, rgba(2, 12, 30, 0.658) 80%);
42
+ opacity: .6;
43
+ transition: opacity 0.3s ease;
44
+ z-index: 2;
45
+ }
46
+
47
+ .film-card:hover .film-card-overlay {
48
+ opacity: 1;
49
+ }
50
+
51
+ .film-card-info {
52
+ height: 100%;
53
+ padding: 10px;
54
+ background-color: rgba(0, 0, 0, 0.807);
55
+ border-top: 1px solid #333;
56
+ z-index: 3;
57
+ position: relative;
58
+ }
59
+
60
+ @import url('https://fonts.googleapis.com/css2?family=Calistoga&family=Pacifico&family=Rubik+Burned&family=Rubik+Marker+Hatch&family=Rubik+Maze&family=Rubik+Microbe&family=Rubik:ital,wght@0,300..900;1,300..900&family=Signika:[email protected]&display=swap');
61
+
62
+ .film-card-title {
63
+ margin: 0;
64
+ font-family: "Signika", sans-serif;
65
+ font-optical-sizing: auto;
66
+ font-style: normal;
67
+ font-variation-settings:
68
+ "GRAD" 0;
69
+ font-weight: 600;
70
+ color: #f5f5f5;
71
+ width: 100%;
72
+ box-sizing: border-box;
73
+ text-align: center;
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+
79
+ .film-card-year {
80
+ margin: 0;
81
+ font-size: .9em;
82
+ font-family: "Signika", sans-serif;
83
+ font-optical-sizing: auto;
84
+ font-style: normal;
85
+ font-variation-settings:
86
+ "GRAD" 0;
87
+ font-weight: 600;
88
+ color: #949494;
89
+ border: none;
90
+ width: 100%;
91
+ box-sizing: border-box;
92
+ text-align: center;
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ }
97
+
98
+ .spinner {
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ margin: auto;
103
+ }
104
+
105
+ .spinner div {
106
+ width: 15px;
107
+ height: 15px;
108
+ background-color: #ff8c00;
109
+ border-radius: 50%;
110
+ animation: spin 1s infinite ease-in-out;
111
+ margin: 0 5px;
112
+ }
113
+
114
+ .spinner div:nth-child(1) {
115
+ animation-delay: -0.32s;
116
+ }
117
+
118
+ .spinner div:nth-child(2) {
119
+ animation-delay: -0.16s;
120
+ }
121
+
122
+ @keyframes spin {
123
+ 0%, 100% {
124
+ transform: translateY(0);
125
+ }
126
+ 50% {
127
+ transform: translateY(-20px);
128
+ }
129
+ }
130
+
131
+ .film-card.loading {
132
+ display: flex;
133
+ flex-direction: column;
134
+ justify-content: center;
135
+ align-items: center;
136
+ text-align: center;
137
+ background-color: #2c2c2c;
138
+ border: 1px solid #333;
139
+ border-radius: 15px;
140
+ padding: 20px;
141
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.5);
142
+ width: 250px;
143
+ height: 420px;
144
+ position: relative;
145
+ }
146
+
147
+ .film-card.loading::before {
148
+ content: '';
149
+ position: absolute;
150
+ top: 0;
151
+ left: 0;
152
+ width: 250px;
153
+ height: 420px;
154
+ background: rgba(0, 0, 0, 0.5);
155
+ border-radius: 15px;
156
+ z-index: 1;
157
+ }
158
+
159
+ .film-card.loading .spinner {
160
+ margin-bottom: 20px;
161
+ }
162
+
163
+ .film-card.loading .film-card-title {
164
+ font-size: 1em;
165
+ color: #f5f5f5;
166
+ width: 100%;
167
+ text-align: center;
168
+ z-index: 2;
169
+ margin-top: 10px;
170
+ }
171
+
172
+ /* Media Queries for Mobile Devices */
173
+ @media (max-width: 768px) {
174
+ .film-card {
175
+ width: 150px;
176
+ height: 265px;
177
+ margin: 10px 0;
178
+ }
179
+ .film-card.loading{
180
+ width: 150px;
181
+ height: 265px;
182
+ margin: 10px 0;
183
+ }
184
+
185
+ .film-card-info {
186
+ padding: 5px;
187
+ }
188
+
189
+ .film-card-title, .film-card-year {
190
+ font-size: 0.8em;
191
+
192
+ }
193
+ }
frontend/src/components/film/card.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { useRouter } from 'next/navigation'; // Use the correct import for Next.js 13
3
+ import apiClient from "@/api/apiClient";
4
+ import './card.css';
5
+
6
+ // Spinner component
7
+ const Spinner = () => (
8
+ <div className="spinner">
9
+ <div className="spinner-bounce1"></div>
10
+ <div className="spinner-bounce2"></div>
11
+ <div className="spinner-bounce3"></div>
12
+ </div>
13
+ );
14
+
15
+ export default function FilmCard({ title }) {
16
+ const [metadata, setMetadata] = useState(null);
17
+ const router = useRouter(); // Use Next.js router
18
+
19
+ useEffect(() => {
20
+ const fetchMetadata = async () => {
21
+ try {
22
+ const response = await apiClient.getFilmMetadataByTitle(title);
23
+ const filmData = response.data; // Adjust based on actual API response
24
+ console.log(filmData);
25
+ setMetadata(filmData);
26
+ } catch (error) {
27
+ console.error("Failed to fetch metadata:", error);
28
+ setMetadata({ error: "Failed to load metadata" });
29
+ }
30
+ };
31
+
32
+ fetchMetadata();
33
+ }, [title]);
34
+
35
+ const handleCardClick = () => {
36
+ router.push(`/film/${title}`); // Use Next.js router
37
+ };
38
+
39
+ const findEnglishTranslation = (translations) => {
40
+ if (translations && Array.isArray(translations.nameTranslations)) {
41
+ const primaryEngTranslation = translations.nameTranslations.find(
42
+ translation => translation.language === 'eng' && translation.isPrimary
43
+ );
44
+
45
+ if (primaryEngTranslation) {
46
+ return primaryEngTranslation.name;
47
+ }
48
+
49
+ const engTranslation = translations.nameTranslations.find(
50
+ translation => translation.language === 'eng'
51
+ );
52
+
53
+ if (engTranslation) {
54
+ return engTranslation.name;
55
+ }
56
+ }
57
+ return null;
58
+ };
59
+
60
+ const eng_title = metadata?.translations ?
61
+ (findEnglishTranslation(metadata.translations) || `${metadata?.name} (${metadata?.year})`) :
62
+ `${metadata?.name} (${metadata?.year})` || title;
63
+
64
+ const imageUrl = metadata?.artworks?.find(artwork => artwork.type === 14)?.thumbnail || "";
65
+ const year = metadata?.year;
66
+
67
+ if (metadata === null)
68
+ return (
69
+ <div className="film-card loading">
70
+ <Spinner />
71
+ <div className="film-card-title">Loading...</div>
72
+ </div>
73
+ );
74
+
75
+ if (metadata.error)
76
+ return (
77
+ <div className="film-card error">
78
+ <p className="film-card-title">Error loading metadata</p>
79
+ </div>
80
+ );
81
+
82
+ return (
83
+ <div className="film-card" onClick={handleCardClick}>
84
+ <img src={imageUrl} alt={eng_title} className="film-card-image" />
85
+ <div className="film-card-overlay"></div>
86
+ <div className="film-card-info">
87
+ <p className="film-card-title">{eng_title}</p>
88
+ <p className="film-card-year">{year}</p>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
frontend/src/context/FilmContext.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { createContext, useState, useContext } from 'react';
4
+
5
+ const FilmContext = createContext();
6
+
7
+ export const useFilmContext = () => useContext(FilmContext);
8
+
9
+ export const FilmProvider = ({ children }) => {
10
+ const [films, setFilms] = useState([]);
11
+
12
+ return (
13
+ <FilmContext.Provider value={{ films, setFilms }}>
14
+ {children}
15
+ </FilmContext.Provider>
16
+ );
17
+ };
frontend/src/context/TvshowContext.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+ import React, { createContext, useContext, useState } from 'react';
3
+
4
+ const TvShowsContext = createContext();
5
+
6
+ export const TvShowsProvider = ({ children }) => {
7
+ const [tvshows, setTvshows] = useState([]);
8
+
9
+ return (
10
+ <TvShowsContext.Provider value={{ tvshows, setTvshows }}>
11
+ {children}
12
+ </TvShowsContext.Provider>
13
+ );
14
+ };
15
+
16
+ export const useTvShowsContext = () => {
17
+ return useContext(TvShowsContext);
18
+ };
frontend/src/lib/LoadBalancer.js ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class LoadBalancerAPI {
2
+ constructor(baseURL) {
3
+ this.baseURL = baseURL;
4
+ this.filmCache = null; // Cache for film store
5
+ this.tvCache = null; // Cache for TV store
6
+ }
7
+
8
+ async getInstances() {
9
+ return this._getRequest('/api/get/instances');
10
+ }
11
+
12
+ async getInstancesHealth() {
13
+ return this._getRequest('/api/get/instances/health');
14
+ }
15
+
16
+ async getMovieByTitle(title) {
17
+ return this._getRequest(`/api/get/film/${encodeURIComponent(title)}`);
18
+ }
19
+
20
+ async getTVEpisode(title, season, episode) {
21
+ return this._getRequest(`/api/get/tv/${encodeURIComponent(title)}/${season}/${episode}`);
22
+ }
23
+
24
+ async getTVStore() {
25
+ const response = await this._getRequest('/api/get/tv/store');
26
+
27
+ if (response && Object.keys(response).length > 0) {
28
+ this.tvCache = response; // Update cache if response is not empty
29
+ }
30
+
31
+ return this.tvCache || {}; // Return cache if response is empty
32
+ }
33
+
34
+ async getFilmStore() {
35
+ const response = await this._getRequest('/api/get/film/store');
36
+
37
+ if (response && Object.keys(response).length > 0) {
38
+ this.filmCache = response; // Update cache if response is not empty
39
+ }
40
+
41
+ return this.filmCache || {}; // Return cache if response is empty
42
+ }
43
+
44
+ async getFilmMetadataByTitle(title) {
45
+ return this._getRequest(`/api/get/film/metadata/${encodeURIComponent(title)}`);
46
+ }
47
+
48
+ async getTVMetadataByTitle(title) {
49
+ return this._getRequest(`/api/get/tv/metadata/${encodeURIComponent(title)}`);
50
+ }
51
+
52
+ async getSeasonMetadataBySeriesId(seires_id, season) {
53
+ return this._getRequest(`/api/get/tv/metadata/${seires_id}/${season}`);
54
+ }
55
+
56
+ async getAllFilms() {
57
+ return this._getRequest('/api/get/film/all');
58
+ }
59
+
60
+ async getAllTVShows() {
61
+ return this._getRequest('/api/get/tv/all');
62
+ }
63
+
64
+ async getDownloadProgress(url) {
65
+ return this._getRequestNoBase(url);
66
+ }
67
+
68
+ // Helper methods for GET and POST requests
69
+ async _getRequest(endpoint) {
70
+ try {
71
+ const response = await fetch(`${this.baseURL}${endpoint}`, {
72
+ method: 'GET',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ });
75
+ console.log(`api endpoint: ${this.baseURL}${endpoint}`);
76
+ return await this._handleResponse(response);
77
+ } catch (error) {
78
+ console.error(`Error during GET request to ${endpoint}:`, error);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ async _postRequest(endpoint, body) {
84
+ try {
85
+ const response = await fetch(`${this.baseURL}${endpoint}`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify(body),
89
+ });
90
+ return await this._handleResponse(response);
91
+ } catch (error) {
92
+ console.error(`Error during POST request to ${endpoint}:`, error);
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ async _handleResponse(response) {
98
+ if (!response.ok) {
99
+ const errorDetails = await response.text();
100
+ throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
101
+ }
102
+ return response.json();
103
+ }
104
+
105
+ async _getRequestNoBase(url) {
106
+ try {
107
+ const response = await fetch(`${url}`, {
108
+ method: 'GET',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ });
111
+ console.log(`api endpoint: ${url}`);
112
+ return await this._handleResponse(response);
113
+ } catch (error) {
114
+ console.error(`Error during GET request to ${url}:`, error);
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+
120
+ export { LoadBalancerAPI };
frontend/src/lib/config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ apiBaseUrl: 'https://hans-den-load-balancer.hf.space',
3
+ searchUrl: 'https://unicone-studio-search.hf.space/api/search',
4
+ version: "0.0.0.1 Alpha",
5
+ };
6
+
7
+ export default config;