diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 579ef283..89691a02 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,9 +6,6 @@ name: Lint ############################# on: push: - branches-ignore: [main] - pull_request: - branches: [main] ############### # Set the Job # diff --git a/.gitignore b/.gitignore index 4c58e1b7..afc76a51 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ yarn-error.* *.tsbuildinfo .vscode/ +eas.json diff --git a/assets/save_story.png b/assets/save_story.png new file mode 100644 index 00000000..65b2ea6e Binary files /dev/null and b/assets/save_story.png differ diff --git a/assets/saved_story.png b/assets/saved_story.png new file mode 100644 index 00000000..46c3e7b0 Binary files /dev/null and b/assets/saved_story.png differ diff --git a/package-lock.json b/package-lock.json index b96fd7e9..68298471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@mui/styled-engine-sc": "^6.0.0-alpha.1", "@mui/system": "^5.14.13", "@react-native-async-storage/async-storage": "^1.18.2", - "@react-native-community/datetimepicker": "7.2.0", + "@react-native-community/datetimepicker": "^7.2.0", "@react-navigation/bottom-tabs": "^6.5.9", "@react-navigation/material-bottom-tabs": "^6.2.17", "@react-navigation/native": "^6.1.8", @@ -33,26 +33,34 @@ "expo": "~49.0.11", "expo-constants": "~14.4.2", "expo-font": "~11.4.0", + "expo-image": "~1.3.5", "expo-linking": "~5.0.2", "expo-router": "^2.0.0", "expo-status-bar": "~1.6.0", "html-entities": "^2.4.0", "react": "18.2.0", + "react-apple-emojis": "^2.2.1", "react-native": "0.72.10", "react-native-dom-parser": "^1.5.3", "react-native-element-dropdown": "^2.10.0", "react-native-elements": "^3.4.3", + "react-native-emoji": "^1.8.0", + "react-native-emojicon": "^1.0.0", "react-native-gesture-handler": "~2.12.0", "react-native-htmlview": "^0.16.0", "react-native-ionicons": "^4.6.5", + "react-native-modal-datetime-picker": "^17.1.0", + "react-native-neat-date-picker": "^1.4.12", "react-native-otp-textinput": "^1.1.3", "react-native-paper": "^5.10.6", + "react-native-paper-dates": "^0.22.3", "react-native-render-html": "^6.3.4", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", "react-native-svg": "13.9.0", "react-native-toast-message": "^2.2.0", + "react-native-ui-datepicker": "^2.0.2", "react-native-url-polyfill": "^2.0.0", "react-native-vector-icons": "^10.0.2", "react-scroll-to-top": "^3.0.0", @@ -11167,6 +11175,14 @@ "react-native": "*" } }, + "node_modules/expo-image": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.3.5.tgz", + "integrity": "sha512-yrIR2mnfIKbKcguoqWK3U5m3zvLPnonvSCabB2ErVGhws8zQs7ILYf+7T08j8U6eFcohjw0CoAFJ6RWNsX2EhA==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-keep-awake": { "version": "12.3.0", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.3.0.tgz", @@ -13663,9 +13679,9 @@ "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==" }, "node_modules/joi": { - "version": "17.12.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -14150,27 +14166,74 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.frompairs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", + "integrity": "sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw==" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==" + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==" + }, "node_modules/log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -15456,6 +15519,14 @@ "node": ">=10.5.0" } }, + "node_modules/node-emoji": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", + "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "dependencies": { + "lodash.toarray": "^4.4.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -16618,6 +16689,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-apple-emojis": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-apple-emojis/-/react-apple-emojis-2.2.1.tgz", + "integrity": "sha512-tgq/+GUR6WsBkkkl0EYgVbaU803IF8GoELcG83cfircrEiyiiIbHqpBXIHyD8YIOecAGgN2ucEG6U/REDR7jvQ==", + "peerDependencies": { + "prop-types": "*", + "react": ">=16.x", + "react-dom": ">=16.x" + } + }, "node_modules/react-devtools-core": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.0.tgz", @@ -16759,6 +16840,14 @@ "react": "18.2.0" } }, + "node_modules/react-native-animatable": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.3.tgz", + "integrity": "sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==", + "dependencies": { + "prop-types": "^15.7.2" + } + }, "node_modules/react-native-dom-parser": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/react-native-dom-parser/-/react-native-dom-parser-1.5.3.tgz", @@ -16799,6 +16888,19 @@ "react-native-vector-icons": ">7.0.0" } }, + "node_modules/react-native-emoji": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/react-native-emoji/-/react-native-emoji-1.8.0.tgz", + "integrity": "sha512-VunKOtYes6eymyWwE7QS3mhmNXksTt2AN92PcGRtmDKLDPjuKrwd5tcJckFUekAK3H+6AMpwYy30CsiCJrDdFQ==", + "dependencies": { + "node-emoji": "1.10.0" + } + }, + "node_modules/react-native-emojicon": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-native-emojicon/-/react-native-emojicon-1.0.0.tgz", + "integrity": "sha512-rK6/7EIf/yNgkB24ujpV8zmmZylbQV+oq4F7YopZ9aSfNKmVKKFmqGOPahPo6OW8b1Dg5dm8caybgfpM2GP4Nw==" + }, "node_modules/react-native-gesture-handler": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz", @@ -16833,6 +16935,105 @@ "react-native": "*" } }, + "node_modules/react-native-modal": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-13.0.1.tgz", + "integrity": "sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==", + "dependencies": { + "prop-types": "^15.6.2", + "react-native-animatable": "1.3.3" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.65.0" + } + }, + "node_modules/react-native-modal-datetime-picker": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/react-native-modal-datetime-picker/-/react-native-modal-datetime-picker-17.1.0.tgz", + "integrity": "sha512-jfTwfaCLtBffYbQ+pOGFLM+J5HmUh3vb9rT0JrrQPjxzecdc8pNYreB1c96+mVuq8bDCvaCdIeuEsslTqLJL0Q==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@react-native-community/datetimepicker": ">=6.7.0", + "react-native": ">=0.65.0" + } + }, + "node_modules/react-native-neat-date-picker": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/react-native-neat-date-picker/-/react-native-neat-date-picker-1.4.12.tgz", + "integrity": "sha512-gnVmLrpnlNqO6F0wMdJOfFFSLdi5Dq62ugnxLAqWu43PGShlpOCW6CNpjJa84QB/Cd0xmRPlHh7+fEOuMtzjYA==", + "dependencies": { + "react-native-modal": "13.0.1", + "react-native-vector-icons": "^8.1.0" + } + }, + "node_modules/react-native-neat-date-picker/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-neat-date-picker/node_modules/react-native-vector-icons": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-8.1.0.tgz", + "integrity": "sha512-sHIdBB6Y0dHaot2fMXgy5J/hhCn5YuyN7SKDNFgPzL8KA1oF2/v7mgYMavnK7LIIs2dJoGnDANKf61dsU+TZlg==", + "dependencies": { + "lodash.frompairs": "^4.0.1", + "lodash.isequal": "^4.5.0", + "lodash.isstring": "^4.0.1", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "lodash.template": "^4.5.0", + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa5-upgrade": "bin/fa5-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-neat-date-picker/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-neat-date-picker/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-neat-date-picker/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/react-native-otp-textinput": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/react-native-otp-textinput/-/react-native-otp-textinput-1.1.3.tgz", @@ -16860,6 +17061,48 @@ "react-native-vector-icons": "*" } }, + "node_modules/react-native-paper-dates": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/react-native-paper-dates/-/react-native-paper-dates-0.22.3.tgz", + "integrity": "sha512-3zmhN09Z06SnYzHKEui7NRzwpxA/CkzlEo57pKiLZIg5TbfqtGu6WPTQZ17CpymfIhhyWZgPWcRTPasBr0XyJA==", + "dependencies": { + "color": "^4.2.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-paper": "*", + "react-native-vector-icons": "*" + } + }, + "node_modules/react-native-paper-dates/node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/react-native-paper-dates/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/react-native-paper-dates/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/react-native-ratings": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.0.4.tgz", @@ -16949,6 +17192,23 @@ "react-native": "*" } }, + "node_modules/react-native-ui-datepicker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-native-ui-datepicker/-/react-native-ui-datepicker-2.0.2.tgz", + "integrity": "sha512-mCV7nj87YXKiPXGC0blYH+kUphUIZfbvJtgDUD8u2FPsvQhJ1G8jVahqNQYURTbiyuZd+okeUTAKUVCH5N2IqQ==", + "dependencies": { + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "uninstall": "^0.0.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-url-polyfill": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz", @@ -18989,6 +19249,11 @@ "node": ">=4" } }, + "node_modules/uninstall": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/uninstall/-/uninstall-0.0.0.tgz", + "integrity": "sha512-pjP/0+A4gsbDVa8XH/S2GZdT9NPJW8NFMy3GI7HnsWG+NAmFSSj3QidNosXBI9cPtxxNExEDdhKFO6sli8K3mA==" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/package.json b/package.json index 7c10cf75..d15d3b50 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@mui/styled-engine-sc": "^6.0.0-alpha.1", "@mui/system": "^5.14.13", "@react-native-async-storage/async-storage": "^1.18.2", - "@react-native-community/datetimepicker": "7.2.0", + "@react-native-community/datetimepicker": "^7.2.0", "@react-navigation/bottom-tabs": "^6.5.9", "@react-navigation/material-bottom-tabs": "^6.2.17", "@react-navigation/native": "^6.1.8", @@ -42,26 +42,34 @@ "expo-status-bar": "~1.6.0", "html-entities": "^2.4.0", "react": "18.2.0", + "react-apple-emojis": "^2.2.1", "react-native": "0.72.10", "react-native-dom-parser": "^1.5.3", "react-native-element-dropdown": "^2.10.0", "react-native-elements": "^3.4.3", + "react-native-emoji": "^1.8.0", + "react-native-emojicon": "^1.0.0", "react-native-gesture-handler": "~2.12.0", "react-native-htmlview": "^0.16.0", "react-native-ionicons": "^4.6.5", + "react-native-modal-datetime-picker": "^17.1.0", + "react-native-neat-date-picker": "^1.4.12", "react-native-otp-textinput": "^1.1.3", "react-native-paper": "^5.10.6", + "react-native-paper-dates": "^0.22.3", "react-native-render-html": "^6.3.4", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", "react-native-svg": "13.9.0", "react-native-toast-message": "^2.2.0", + "react-native-ui-datepicker": "^2.0.2", "react-native-url-polyfill": "^2.0.0", "react-native-vector-icons": "^10.0.2", "react-scroll-to-top": "^3.0.0", "use-debounce": "^10.0.0", - "validator": "^13.11.0" + "validator": "^13.11.0", + "expo-image": "~1.3.5" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index 3040918a..de7b449d 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -1,5 +1,5 @@ import { Tabs } from 'expo-router'; -import { Platform } from 'react-native'; +import { Platform, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import Icon from '../../../assets/icons'; @@ -55,7 +55,6 @@ function TabNav() { headerShown: false, tabBarLabel: 'Home', tabBarIcon: ({ color }) => HomeIcon({ color }), - // tabBarLabelStyle: { borderTopWidth: 12, paddingTop: 12 }, }} /> LibraryIcon({ color }), }} /> + + ); } diff --git a/src/app/(tabs)/author/index.tsx b/src/app/(tabs)/author/index.tsx index 9b8065c5..7767637c 100644 --- a/src/app/(tabs)/author/index.tsx +++ b/src/app/(tabs)/author/index.tsx @@ -6,7 +6,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import BackButton from '../../../components/BackButton/BackButton'; -import ContentCard from '../../../components/ContentCard/ContentCard'; import HorizontalLine from '../../../components/HorizontalLine/HorizontalLine'; import PreviewCard from '../../../components/PreviewCard/PreviewCard'; import { @@ -15,6 +14,7 @@ import { } from '../../../queries/authors'; import { Author, StoryPreview } from '../../../queries/types'; import globalStyles from '../../../styles/globalStyles'; +import * as cheerio from 'cheerio'; function AuthorScreen() { const [authorInfo, setAuthorInfo] = useState(); @@ -27,41 +27,39 @@ function AuthorScreen() { useEffect(() => { setLoading(true); (async () => { - const storyData: StoryPreview[] = await fetchAuthorStoryPreviews( - parseInt(author as string, 10), - ); - const authorData: Author = await fetchAuthor( - parseInt(author as string, 10), - ); try { - setAuthorInfo(authorData); - console.log('TESTING AUTHOR INFO QUERY OUTPUT:', authorInfo); - } catch (error) { - console.log( - `There was an error while trying to output authorinfo ${error}`, + const storyData: StoryPreview[] = await fetchAuthorStoryPreviews( + parseInt(author as string, 10), ); - } - try { + const authorData: Author = await fetchAuthor( + parseInt(author as string, 10), + ); + + // Assuming these setters do not throw, but if they do, they're caught by the catch block + setAuthorInfo(authorData); setAuthorStoryPreview(storyData); - console.log('TESTING STORY PREVIEW INFO QUERY OUTPUT:', storyData); } catch (error) { - console.log( - `There was an error while trying to output author story preview info ${error}`, - ); + console.error('There was an error while fetching data:', error); + } finally { + setLoading(false); } - })().then(() => { - setLoading(false); - }); + })(); }, [author]); + const getTextFromHtml = (text: string) => { + return cheerio.load(text).text().trim(); + }; + return ( - + {isLoading ? ( ) : ( router.back()} /> @@ -76,12 +74,14 @@ function AuthorScreen() { - {authorInfo.name} + {getTextFromHtml(authorInfo.name)} {authorInfo?.pronouns && ( - {authorInfo.pronouns} + + {authorInfo.pronouns} + )} )} @@ -91,18 +91,22 @@ function AuthorScreen() { {authorInfo?.bio && ( <> - {decode(authorInfo.bio)} + + {getTextFromHtml(authorInfo.bio)} + )} {authorInfo?.artist_statement && ( <> - + Artist's Statement - - {decode(authorInfo.artist_statement)} + + {getTextFromHtml(authorInfo.artist_statement)} @@ -118,6 +122,7 @@ function AuthorScreen() { {authorStoryPreview?.map(story => ( ))} + + {/* View so there's space between the tab bar and the stories */} + )} diff --git a/src/app/(tabs)/author/styles.tsx b/src/app/(tabs)/author/styles.tsx index d85a4dd7..213b9e7b 100644 --- a/src/app/(tabs)/author/styles.tsx +++ b/src/app/(tabs)/author/styles.tsx @@ -10,28 +10,12 @@ const styles = StyleSheet.create({ justifyContent: 'flex-start', alignItems: 'flex-end', }, - name: { - fontWeight: 'bold', - fontSize: 24, - fontFamily: 'Manrope-Regular', - }, image: { height: 68, width: 68, backgroundColor: colors.darkGrey, borderRadius: 4, }, - bioText: { - color: 'black', - fontFamily: 'Manrope-Regular', - fontSize: 14, - }, - authorStatement: { - fontSize: 14, - color: 'black', - fontWeight: '400', - fontFamily: 'Manrope-Regular', - }, authorTextContainer: { paddingLeft: 20, }, @@ -40,9 +24,6 @@ const styles = StyleSheet.create({ borderTopWidth: 20, }, authorStatementTitle: { - fontWeight: 'bold', - fontFamily: 'Manrope-Regular', - fontSize: 16, marginBottom: 8, }, storyCountText: { @@ -50,7 +31,7 @@ const styles = StyleSheet.create({ marginBottom: 8, }, pronouns: { - color: '#797979', + color: colors.textGrey, }, }); diff --git a/src/app/settings/_layout.tsx b/src/app/(tabs)/genre/_layout.tsx similarity index 100% rename from src/app/settings/_layout.tsx rename to src/app/(tabs)/genre/_layout.tsx diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx new file mode 100644 index 00000000..87712eb5 --- /dev/null +++ b/src/app/(tabs)/genre/index.tsx @@ -0,0 +1,352 @@ +import { useLocalSearchParams, router } from 'expo-router'; +import { useEffect, useState, useMemo, ReactNode } from 'react'; +import { + ActivityIndicator, + ScrollView, + View, + Text, + FlatList, +} from 'react-native'; +import { MultiSelect } from 'react-native-element-dropdown'; +import { Icon } from 'react-native-elements'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import styles from './styles'; +import BackButton from '../../../components/BackButton/BackButton'; +import { fetchGenreStoryById } from '../../../queries/genres'; +import { fetchStoryPreviewByIds } from '../../../queries/stories'; +import { StoryPreview, GenreStories } from '../../../queries/types'; +import globalStyles from '../../../styles/globalStyles'; +import PreviewCard from '../../../components/PreviewCard/PreviewCard'; + +function GenreScreen() { + const [genreStoryData, setGenreStoryData] = useState(); + const [genreStoryIds, setGenreStoryIds] = useState([]); + const [subgenres, setSubgenres] = useState([]); + const [allStoryPreviews, setAllStoryPreviews] = useState([]); + const [filteredStoryPreviews, setFilteredStoryPreviews] = useState< + StoryPreview[] + >([]); + const [selectedSubgenre, setSelectedSubgenre] = useState(''); + const [mainGenre, setMainGenre] = useState(''); + const [isLoading, setLoading] = useState(true); + const [toneFilterOptions, setToneFilterOptions] = useState([]); + const [topicFilterOptions, setTopicFilterOptions] = useState([]); + const [selectedTonesForFiltering, setSelectedTonesForFiltering] = useState< + string[] + >([]); + const [selectedTopicsForFiltering, setSelectedTopicsForFiltering] = useState< + string[] + >([]); + const { genreId, genreType, genreName } = useLocalSearchParams<{ + genreId: string; + genreType: GenreType; + genreName: string; + }>(); + + useEffect(() => { + const checkTopic = (preview: StoryPreview): boolean => { + if (preview == null || preview.topic == null) return false; + if (selectedTopicsForFiltering.length == 0) return true; + else + return selectedTopicsForFiltering.every(t => preview.topic.includes(t)); + }; + const checkTone = (preview: StoryPreview): boolean => { + if (preview == null || preview.tone == null) return false; + if (selectedTonesForFiltering.length == 0) return true; + else + return selectedTonesForFiltering.every(t => preview.tone.includes(t)); + }; + + const filteredPreviews = allStoryPreviews.filter( + preview => checkTopic(preview) && checkTone(preview), + ); + setFilteredStoryPreviews(filteredPreviews); + }, [selectedTopicsForFiltering, selectedTonesForFiltering]); + + function getAllStoryIds(genreStories: GenreStories[]): string[] { + return genreStories + .map(story => story.genre_story_previews) + .flat() + .filter(story => story !== null); + } + + function filterStoriesBySubgenreName( + subgenreName: string, + stories: GenreStories[], + ): string[] { + const matchingGenreStory = stories.find( + subgenre => subgenre.subgenre_name === subgenreName, + ); + + return matchingGenreStory?.genre_story_previews ?? []; + } + + function getSubgenres(stories: GenreStories[]): string[] { + const subgenres = stories.map(subgenre => subgenre.subgenre_name); + return ['All', ...subgenres]; + } + + function filterBySubgenre(subgenre: string) { + setLoading(true); + setSelectedSubgenre(subgenre); + if (!genreStoryData) { + setLoading(false); + return []; + } + + if (subgenre === 'All') { + setGenreStoryIds(getAllStoryIds(genreStoryData)); + } else { + const filteredStoryIds = filterStoriesBySubgenreName( + subgenre, + genreStoryData, + ); + + setGenreStoryIds(filteredStoryIds); + setToneFilterOptions([]); + setTopicFilterOptions([]); + setLoading(false); + } + } + + useEffect(() => { + const getGenre = async () => { + setLoading(true); + + const genreStoryData: GenreStories[] = await fetchGenreStoryById( + parseInt(genreId as string, 10), + ); + + setGenreStoryData(genreStoryData); + setMainGenre(genreStoryData[0].parent_name); + setSubgenres(getSubgenres(genreStoryData)); + + if (genreType == GenreType.PARENT) { + setSelectedSubgenre('All'); //if user clicks see all, selected should be 'ALL' + setGenreStoryIds(getAllStoryIds(genreStoryData)); + } else if (genreType == GenreType.SUBGENRE) { + setSelectedSubgenre(genreName || ''); //if user clicks a specific genre, selected should be genreName + + const filteredStoryIds = filterStoriesBySubgenreName( + genreName || '', + genreStoryData, + ); + setGenreStoryIds(filteredStoryIds); + + setLoading(false); + } + }; + getGenre(); + }, [genreName]); + + useEffect(() => { + const showAllStoryPreviews = async () => { + setLoading(true); + + const previews: StoryPreview[] = await fetchStoryPreviewByIds( + genreStoryIds as any, + ); + + const tones: string[] = previews + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.tone); + }, [] as string[]) + .filter(tone => tone !== null); + const topics: string[] = previews + .reduce((acc: string[], current: StoryPreview) => { + return acc.concat(current.topic); + }, [] as string[]) + .filter(topic => topic !== null); + + setAllStoryPreviews(previews.flat()); + setFilteredStoryPreviews(previews.flat()); + setTopicFilterOptions([...new Set(topics)]); + setToneFilterOptions([...new Set(tones)]); + + setLoading(false); + }; + + if (genreStoryIds.length > 0) { + showAllStoryPreviews(); + } + }, [genreStoryIds]); + + const renderGenreScrollSelector = () => { + return ( + + {subgenres.map((subgenre, index) => ( + filterBySubgenre(subgenre)} //onPress will trigger the filterBySubgenre function + style={{ paddingHorizontal: 20, paddingTop: 5 }} + key={index} + > + + {subgenre} + + + ))} + + ); + }; + + const renderGenreHeading = () => { + return ( + + + {selectedSubgenre === 'All' ? mainGenre : selectedSubgenre} + + {/* */} + {/* {' '} */} + {/* Subheading about{' '} */} + {/* {selectedSubgenre === 'All' ? mainGenre : selectedSubgenre} */} + {/* ...Include Later? */} + {/* */} + + ); + }; + + const renderFilterDropdown = ( + placeholder: string, + value: string[], + data: string[], + setter: React.Dispatch>, + ) => { + return ( + { + return { label: topic, value: topic }; + })} + renderSelectedItem={() => } + maxHeight={400} + labelField="label" + valueField="value" + placeholder={placeholder} + renderRightIcon={() => } + onChange={item => { + if (item) { + setter(item); + } + }} + /> + ); + }; + + const renderNoStoryText = () => { + return ( + + Sorry! + + There are no stories under this Genre or Subgenre. Please continue to + search for other stories + + + ); + }; + + const renderStories = () => { + return ( + ( + { + router.push({ + pathname: '/story', + params: { storyId: item.id.toString() }, + }); + }} + /> + )} + /> + ); + }; + + return ( + + + + + router.push({ + pathname: '/search', + }) + } + /> + + {useMemo(renderGenreHeading, [selectedSubgenre, mainGenre])} + {useMemo(renderGenreScrollSelector, [subgenres, selectedSubgenre])} + + + + {renderFilterDropdown( + 'Tone', + selectedTonesForFiltering, + toneFilterOptions, + setSelectedTonesForFiltering, + )} + {renderFilterDropdown( + 'Topic', + selectedTopicsForFiltering, + topicFilterOptions, + setSelectedTopicsForFiltering, + )} + + + {genreStoryIds.length === 0 && !isLoading ? ( + renderNoStoryText() + ) : ( + <> + + {isLoading ? ( + + + + ) : ( + renderStories() + )} + + + )} + + + ); +} + +export enum GenreType { + PARENT = 'parent', + SUBGENRE = 'subgenre', +} + +export default GenreScreen; diff --git a/src/app/(tabs)/genre/styles.tsx b/src/app/(tabs)/genre/styles.tsx new file mode 100644 index 00000000..1869ac12 --- /dev/null +++ b/src/app/(tabs)/genre/styles.tsx @@ -0,0 +1,72 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../../styles/colors'; + +const styles = StyleSheet.create({ + textSelected: { + color: '#EB563B', + textDecorationLine: 'underline', + }, + container: { + paddingHorizontal: 24, + width: '100%', + marginTop: 24, + flex: 1, + }, + + flatListStyle: { + paddingTop: 15, + }, + scrollViewContainer: { + marginVertical: 15, + width: '100%', + }, + noStoriesText: { + fontSize: 20, + color: '#EB563B', + }, + noStoriesText2: { + fontSize: 13, + }, + renderStories: { + paddingBottom: 10, + flex: 1, + }, + headerContainer: {}, + dropdown: { + borderColor: '#797979', + flexGrow: 0, + flexShrink: 0, + borderWidth: 1.5, + borderRadius: 7, + width: 140, + height: 30, + color: '#797979', + }, + dropdownContainer: { + marginTop: 20, + marginBottom: 20, + flexDirection: 'row', + justifyContent: 'flex-start', + }, + firstDropdown: { + marginRight: 10, + }, + secondDropdown: { + marginLeft: 10, + }, + icon: { + marginRight: 5, + }, + iconStyle: { + width: 20, + height: 20, + }, + itemContainer: {}, + placeholderStyle: { + color: colors.darkGrey, + marginLeft: 45, + }, +}); + +export default styles; diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index 65104b05..8a1f8aee 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -1,6 +1,12 @@ import { router } from 'expo-router'; import { useEffect, useState } from 'react'; -import { Pressable, ScrollView, Text, View } from 'react-native'; +import { + ActivityIndicator, + Pressable, + ScrollView, + Text, + View, +} from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; @@ -17,7 +23,6 @@ import { import { StoryCard, StoryPreview } from '../../../queries/types'; import globalStyles from '../../../styles/globalStyles'; import { useSession } from '../../../utils/AuthContext'; -import TestCard from '../../../components/TestCard/TestCard'; function HomeScreen() { const { user } = useSession(); @@ -44,6 +49,7 @@ function HomeScreen() { fetchRecommendedStories().catch(() => []), fetchNewStories().catch(() => []), ]); + setUsername(usernameResponse); setFeaturedStories(featuredStoryResponse); setFeaturedStoriesDescription(featuredStoryDescriptionResponse); @@ -54,26 +60,27 @@ function HomeScreen() { }); }, [user]); + if (loading) { + return ; + } return ( - {loading && ( - - Loading - - )} - - + {username ? `Welcome, ${username}` : 'Welcome!'} + {featuredStories.length > 0 && ( Featured Stories @@ -83,7 +90,8 @@ function HomeScreen() { {featuredStories.map(story => ( {recommendedStories.map(story => ( router.push({ pathname: '/story', @@ -140,13 +150,15 @@ function HomeScreen() { horizontal showsHorizontalScrollIndicator={false} bounces={false} - style={styles.scrollView} + style={styles.scrollView2} > {newStories.map(story => ( router.push({ pathname: '/story', diff --git a/src/app/(tabs)/home/styles.ts b/src/app/(tabs)/home/styles.ts index 231f5d9d..9f75b42d 100644 --- a/src/app/(tabs)/home/styles.ts +++ b/src/app/(tabs)/home/styles.ts @@ -19,9 +19,15 @@ const styles = StyleSheet.create({ marginTop: 12, marginBottom: 16, }, - scrollView: { - marginBottom: 20, + scrollView1: { + paddingBottom: 16, flexGrow: 0, + padding: 8, + }, + scrollView2: { + paddingBottom: 20, + flexGrow: 0, + padding: 8, }, headerContainer: { flexDirection: 'row', diff --git a/src/app/(tabs)/search/index.tsx b/src/app/(tabs)/search/index.tsx index 23450af4..fdc5612d 100644 --- a/src/app/(tabs)/search/index.tsx +++ b/src/app/(tabs)/search/index.tsx @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { SearchBar } from '@rneui/themed'; import { router } from 'expo-router'; -import { useEffect, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { Button, FlatList, @@ -9,6 +9,7 @@ import { Text, ScrollView, Pressable, + TouchableOpacity, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -22,13 +23,14 @@ import { fetchAllStoryPreviews } from '../../../queries/stories'; import { StoryPreview, RecentSearch, Genre } from '../../../queries/types'; import colors from '../../../styles/colors'; import globalStyles from '../../../styles/globalStyles'; +import { GenreType } from '../genre'; const getRecentSearch = async () => { try { const jsonValue = await AsyncStorage.getItem('GWN_RECENT_SEARCHES_ARRAY'); return jsonValue != null ? JSON.parse(jsonValue) : []; } catch (error) { - console.log(error); + console.error(error); } }; @@ -37,7 +39,7 @@ const setRecentSearch = async (searchResult: RecentSearch[]) => { const jsonValue = JSON.stringify(searchResult); await AsyncStorage.setItem('GWN_RECENT_SEARCHES_ARRAY', jsonValue); } catch (error) { - console.log(error); + console.error(error); } }; @@ -46,7 +48,7 @@ const getRecentStory = async () => { const jsonValue = await AsyncStorage.getItem('GWN_RECENT_STORIES_ARRAY'); return jsonValue != null ? JSON.parse(jsonValue) : []; } catch (error) { - console.log(error); + console.error(error); } }; @@ -55,7 +57,7 @@ const setRecentStory = async (recentStories: StoryPreview[]) => { const jsonValue = JSON.stringify(recentStories); await AsyncStorage.setItem('GWN_RECENT_STORIES_ARRAY', jsonValue); } catch (error) { - console.log(error); + console.error(error); } }; @@ -69,20 +71,24 @@ function SearchScreen() { const [showGenreCarousals, setShowGenreCarousals] = useState(true); const [showRecents, setShowRecents] = useState(false); const [recentlyViewed, setRecentlyViewed] = useState([]); + const genreColors = [colors.citrus, colors.lime, colors.lilac]; useEffect(() => { (async () => { - const data: StoryPreview[] = await fetchAllStoryPreviews(); - setAllStories(data); - const genreData: Genre[] = await fetchGenres(); - setAllGenres(genreData); - setRecentSearches(await getRecentSearch()); - setRecentlyViewed(await getRecentStory()); + fetchAllStoryPreviews().then((stories: StoryPreview[]) => + setAllStories(stories), + ); + fetchGenres().then((genres: Genre[]) => setAllGenres(genres)); + getRecentSearch().then((searches: RecentSearch[]) => + setRecentSearches(searches), + ); + getRecentStory().then((viewed: StoryPreview[]) => + setRecentlyViewed(viewed), + ); })(); }, []); const getColor = (index: number) => { - const genreColors = [colors.citrus, colors.lime, colors.lilac]; return genreColors[index % genreColors.length]; }; @@ -92,12 +98,14 @@ function SearchScreen() { setSearchResults([]); return; } + const updatedData = allStories.filter((item: StoryPreview) => { const title = `${item.title.toUpperCase()})`; const author = `${item.author_name.toUpperCase()})`; const text_data = text.toUpperCase(); return title.indexOf(text_data) > -1 || author.indexOf(text_data) > -1; }); + setSearch(text); setSearchResults(updatedData); setShowGenreCarousals(false); @@ -175,7 +183,7 @@ function SearchScreen() { return ( searchFunction(text)} value={search} onSubmitEditing={searchString => { @@ -222,19 +233,44 @@ function SearchScreen() { )} {showRecents && - (search ? ( + (search && searchResults.length > 0 ? ( - - {searchResults.length}{' '} - {searchResults.length === 1 ? 'Story' : 'Stories'} + + Showing results 1-{searchResults.length} - ) : ( + ) : search && searchResults.length === 0 ? ( + + + + There are no stories + + + for "{search}". + + + + Try searching by title or author, or + + + check if your spelling is correct. + + + ) : recentSearches.length > 0 || recentlyViewed.length > 0 ? ( - Recent Searches + + Recent Searches + - Clear All + + Clear All + @@ -252,15 +288,25 @@ function SearchScreen() { - Recently Viewed + + Recently Viewed + - Clear All + + Clear All + {recentlyViewed.map(item => ( + ) : ( + + + + Find stories from young creators. + + + + Search for stories, authors, or collections. + + ))} {showGenreCarousals ? ( @@ -286,10 +343,23 @@ function SearchScreen() { contentContainerStyle={{ paddingHorizontal: 8 }} > {allGenres.map((genre, index) => ( - <> + {genre.parent_name} - See All + { + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.PARENT, + genreName: genre.parent_name, + }, + }); + }} + > + See All + {genre.subgenres.map(subgenre => ( null} + pressFunction={() => { + router.push({ + pathname: '/genre', + params: { + genreId: genre.parent_id.toString(), + genreType: GenreType.SUBGENRE, + genreName: subgenre.name, + }, + }); + }} /> ))} - + ))} ) : ( @@ -315,7 +396,8 @@ function SearchScreen() { contentContainerStyle={styles.contentCotainerStories} renderItem={({ item }) => ( + + + ); +} + +export default StackLayout; diff --git a/src/app/(tabs)/settings/index.tsx b/src/app/(tabs)/settings/index.tsx new file mode 100644 index 00000000..0434bc6e --- /dev/null +++ b/src/app/(tabs)/settings/index.tsx @@ -0,0 +1,307 @@ +import { Redirect, router, Link } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { + Text, + View, + Alert, + Platform, + Pressable, + Appearance, +} from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Icon } from 'react-native-elements'; + +import styles from './styles'; +import colors from '../../../styles/colors'; +import AccountDataDisplay from '../../../components/AccountDataDisplay/AccountDataDisplay'; +import StyledButton from '../../../components/StyledButton/StyledButton'; +import UserSelectorInput from '../../../components/UserSelectorInput/UserSelectorInput'; +import globalStyles from '../../../styles/globalStyles'; +import { useSession } from '../../../utils/AuthContext'; +import supabase from '../../../utils/supabase'; +import DateTimePickerModal from 'react-native-modal-datetime-picker'; + +function SettingsScreen() { + const { session, signOut } = useSession(); + const [loading, setLoading] = useState(true); + const [firstName, setFirstName] = useState(''); + const [username, setUsername] = useState(''); + const [lastName, setLastName] = useState(''); + const [pronouns, setPronouns] = useState(''); + const [birthday, setBirthday] = useState(''); + const [displayDate, setDisplayDate] = useState(new Date(2000, 0)); + const [birthdayExists, setBirthdayExists] = useState(false); + const [birthdayChanged, setBirthdayChanged] = useState(false); + const [gender, setGender] = useState(''); + const [raceEthnicity, setRaceEthnicity] = useState(''); + //check if phone is in dark mode + const colorScheme = Appearance.getColorScheme(); + const [isDark, setIsDark] = useState(colorScheme === 'dark'); + + const [showSaveEdits, setShowSaveEdits] = useState(false); + const [showDatePicker, setShowDatePicker] = useState(false); + + const wrapInDetectChange = (onChange: (_: any) => any) => { + return (value: any) => { + setShowSaveEdits(true); + return onChange(value); + }; + }; + + const getProfile = async () => { + try { + setLoading(true); + if (!session?.user) throw new Error('No user on the session!'); + + const { data, error, status } = await supabase + .from('profiles') + .select( + `first_name, last_name, username, birthday, gender, race_ethnicity, pronouns`, + ) + .eq('user_id', session?.user.id) + .single(); + + if (error && status !== 406 && error instanceof Error) { + throw error; + } + + if (data) { + setFirstName(data.first_name || firstName); + setLastName(data.last_name || lastName); + setUsername(data.username || username); + + if (data.birthday) { + setBirthday( + new Date(data.birthday).toLocaleDateString('en-US', { + timeZone: 'UTC', + }), + ); + setBirthdayExists(true); + } + + setGender(data.gender || gender); + setPronouns(data.pronouns || pronouns); + setRaceEthnicity(data.race_ethnicity || raceEthnicity); + } + } catch (error) { + if (error instanceof Error) { + Alert.alert(`Get profile error: ${error.message}`); + } + } finally { + setLoading(false); + } + }; + + const resetAndPushToRouter = (path: string) => { + while (router.canGoBack()) { + router.back(); + } + router.replace(path); + }; + + useEffect(() => { + if (session) getProfile(); + }, [session]); + + useEffect(() => { + if (!session) resetAndPushToRouter('/auth/login'); + }, [session]); + + const updateProfile = async () => { + try { + setLoading(true); + if (!session?.user) throw new Error('No user on the session!'); + + // Only update values that are not blank + const updates = { + ...(gender && { gender }), + ...(pronouns && { pronouns }), + ...(raceEthnicity && { race_ethnicity: raceEthnicity }), + ...(birthday && { birthday }), + }; + + // Check if user exists + const { count } = await supabase + .from('profiles') + .select(`*`, { count: 'exact' }) + .eq('user_id', session?.user.id); + + if (count && count >= 1) { + // Update user if they exist + const { error } = await supabase + .from('profiles') + .update(updates) + .eq('user_id', session?.user.id) + .select('*'); + + if (error && error instanceof Error) { + if (process.env.NODE_ENV !== 'production') { + throw error; + } + } + } else { + // Create user if they don't exist + const { error } = await supabase.from('profiles').insert(updates); + + if (error && error instanceof Error) { + if (process.env.NODE_ENV !== 'production') { + throw error; + } + } + } + } catch (error) { + if (error instanceof Error) { + Alert.alert(error.message); + } + } finally { + setLoading(false); + setShowSaveEdits(false); + setBirthdayExists(true); + setBirthdayChanged(false); + } + }; + + const onConfirmDate = (date: Date) => { + setShowDatePicker(false); + setBirthday(date.toLocaleDateString()); + setDisplayDate(date); + setShowSaveEdits(true); + setBirthdayChanged(true); + }; + + if (!session) { + return ; + } + + return ( + + + + + + {' + + + setShowDatePicker(false)} + date={displayDate} + display="inline" + isDarkModeEnabled={isDark} + themeVariant={isDark ? 'dark' : 'light'} + /> + + + Settings + Account + + + + + + + { + setShowDatePicker(!showDatePicker); + }} + > + + + {birthdayChanged ? birthday : 'Select Date'} + + + + + + ) : ( + + {birthday} + + ) + } + /> + + + + + + + + {birthdayChanged && ( + + + + You can only input your birthday once. Please make sure the date + is correct before saving as you will not be able to change your + birthday in the future. + + + )} + + + + {showSaveEdits ? ( + + ) : ( + + )} + + + + ); +} + +export default SettingsScreen; diff --git a/src/app/(tabs)/settings/styles.tsx b/src/app/(tabs)/settings/styles.tsx new file mode 100644 index 00000000..cded918b --- /dev/null +++ b/src/app/(tabs)/settings/styles.tsx @@ -0,0 +1,69 @@ +import { StyleSheet } from 'react-native'; +import colors from '../../../styles/colors'; + +export default StyleSheet.create({ + selectors: { + flex: 1, + gap: 16, + }, + container: { + flex: 1, + backgroundColor: 'white', + paddingHorizontal: 24, + paddingBottom: 60, + }, + button: { + marginBottom: 32, + }, + main: { + paddingLeft: 12, + width: '100%', + justifyContent: 'space-between', + flexGrow: 1, + }, + subheading: { + paddingBottom: 16, + }, + heading: { + paddingBottom: 20, + }, + back: { + paddingTop: 30, + paddingBottom: 16, + color: '#797979', + fontSize: 12, + fontWeight: '400', + }, + backText: { + color: colors.darkGrey, + }, + staticData: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'flex-start', + marginBottom: 6, + }, + icon: { + paddingLeft: 8, + }, + dateButtonText: { + flexDirection: 'row', + alignItems: 'flex-start', + }, + dateButton: { + paddingTop: 18, + }, + info: { + flexDirection: 'row', + marginLeft: 8, + marginTop: 40, + marginBottom: 20, + marginRight: 30, + maxWidth: '80%', + }, + subtext: { + color: colors.darkGrey, + marginLeft: 8, + flexWrap: 'wrap', + }, +}); diff --git a/src/app/(tabs)/story/index.tsx b/src/app/(tabs)/story/index.tsx index 646a1e4a..2387f2f1 100644 --- a/src/app/(tabs)/story/index.tsx +++ b/src/app/(tabs)/story/index.tsx @@ -18,6 +18,8 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import { fetchStory } from '../../../queries/stories'; import { Story } from '../../../queries/types'; +import colors from '../../../styles/colors'; +import globalStyles, { fonts } from '../../../styles/globalStyles'; function StoryScreen() { const [isLoading, setLoading] = useState(true); @@ -63,7 +65,7 @@ function StoryScreen() { }; return ( - + {isLoading ? ( ) : ( @@ -72,9 +74,8 @@ function StoryScreen() { ref={scrollRef} showsVerticalScrollIndicator={false} > - {/* */} + {story?.title} - {story?.title} { router.push({ @@ -88,7 +89,9 @@ function StoryScreen() { style={styles.authorImage} source={{ uri: story.author_image ? story.author_image : '' }} /> - By {story.author_name} + + By {story.author_name} + @@ -99,50 +102,63 @@ function StoryScreen() { data={story.genre_medium} renderItem={({ item }) => ( - {item} + + {item} + )} /> - Author's Process + + Author's Process + @@ -150,7 +166,9 @@ function StoryScreen() { style={styles.authorImage} source={{ uri: story.author_image }} /> - By {story.author_name} + + By {story.author_name} + )} diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index cbe432a8..66932cf5 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -1,20 +1,12 @@ import { StyleSheet } from 'react-native'; +import colors from '../../../styles/colors'; const styles = StyleSheet.create({ container: { - flex: 1, - backgroundColor: 'white', - alignItems: 'flex-start', - justifyContent: 'flex-start', paddingLeft: 24, paddingRight: 24, paddingTop: 48, }, - image: { - width: '100%', - height: 153, - marginBottom: 16, - }, authorImage: { backgroundColor: '#D9D9D9', width: 21, @@ -22,11 +14,6 @@ const styles = StyleSheet.create({ borderRadius: 100 / 2, }, title: { - fontFamily: 'Manrope-Regular', - fontSize: 24, - fontWeight: '400', - textAlign: 'left', - color: 'black', marginBottom: 16, }, author: { @@ -35,13 +22,6 @@ const styles = StyleSheet.create({ gap: 10, marginBottom: 16, }, - authorText: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '400', - textAlign: 'left', - color: 'black', - }, genres: { display: 'flex', flexDirection: 'row', @@ -59,61 +39,24 @@ const styles = StyleSheet.create({ marginRight: 8, }, genresText: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '400', - color: 'black', backgroundColor: '#D9D9D9', }, shareButtonText: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '400', - textAlign: 'left', - color: 'black', - textDecorationLine: 'underline', - backgroundColor: '#D9D9D9', + color: colors.white, }, excerpt: { - fontFamily: 'Manrope-Regular', - fontSize: 16, - fontWeight: '400', textAlign: 'left', - color: 'black', - paddingTop: 16, - paddingBottom: 16, + paddingVertical: 16, }, story: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '400', - textAlign: 'left', - color: 'black', marginBottom: 16, }, authorProcess: { - fontFamily: 'Manrope-Regular', - fontSize: 16, - fontWeight: '600', - textAlign: 'left', - color: 'black', marginBottom: 16, }, process: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '400', - textAlign: 'left', - color: 'black', marginBottom: 16, }, - backToTopButtonText: { - fontFamily: 'Manrope-Regular', - fontSize: 12, - fontWeight: '800', - textAlign: 'left', - color: 'black', - }, }); export default styles; diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 7e805eaa..b8f05279 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -2,6 +2,7 @@ import { Stack } from 'expo-router'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { AuthContextProvider } from '../utils/AuthContext'; +import { BooleanPubSubProvider } from '../utils/PubSubContext'; import ToastComponent from '../components/Toast/Toast'; import { Keyboard, TouchableWithoutFeedback } from 'react-native'; @@ -9,12 +10,13 @@ function StackLayout() { return ( - - - - - - + + + + + + + diff --git a/src/app/auth/forgotPassword/index.tsx b/src/app/auth/forgotPassword/index.tsx index 99692e48..45570132 100644 --- a/src/app/auth/forgotPassword/index.tsx +++ b/src/app/auth/forgotPassword/index.tsx @@ -25,7 +25,11 @@ function ForgotPasswordScreen() { const { error } = await resetPassword(emailToReset); if (error) Alert.alert('Could not send a reset password email. Please try again.'); - else router.replace('auth/signup'); + else + router.push({ + pathname: '/auth/verify', + params: { finalRedirect: 'resetPassword', userEmail: emailToReset }, + }); }; useEffect(() => { diff --git a/src/app/auth/onboarding/index.tsx b/src/app/auth/onboarding/index.tsx index bfc85fb8..ea0af6ed 100644 --- a/src/app/auth/onboarding/index.tsx +++ b/src/app/auth/onboarding/index.tsx @@ -1,16 +1,25 @@ -import DateTimePicker from '@react-native-community/datetimepicker'; -import { Redirect, router } from 'expo-router'; +import { Link, Redirect, router } from 'expo-router'; import { useState, useEffect } from 'react'; -import { Alert, ScrollView, Platform, Text, View } from 'react-native'; +import { + Alert, + ScrollView, + Text, + View, + Pressable, + Appearance, +} from 'react-native'; import { Icon } from 'react-native-elements'; +import DateTimePickerModal from 'react-native-modal-datetime-picker'; import styles from './styles'; import StyledButton from '../../../components/StyledButton/StyledButton'; import UserSelectorInput from '../../../components/UserSelectorInput/UserSelectorInput'; +import UserStringInput from '../../../components/UserStringInput/UserStringInput'; +import colors from '../../../styles/colors'; import globalStyles from '../../../styles/globalStyles'; import { useSession } from '../../../utils/AuthContext'; import supabase from '../../../utils/supabase'; -// import DatePicker from '../../../components/DatePicker/DatePicker'; +import { SafeAreaView } from 'react-native-safe-area-context'; function OnboardingScreen() { const { session, user } = useSession(); @@ -19,10 +28,14 @@ function OnboardingScreen() { const [username, setUsername] = useState(''); const [lastName, setLastName] = useState(''); const [pronouns, setPronouns] = useState(''); - const [birthday, setBirthday] = useState(new Date()); + const [birthday, setBirthday] = useState(''); + const [birthdayExists, setBirthdayExists] = useState(false); const [gender, setGender] = useState(''); const [raceEthnicity, setRaceEthnicity] = useState(''); - const [showDatePicker, setShowDatePicker] = useState(Platform.OS === 'ios'); + const [showDatePicker, setShowDatePicker] = useState(false); + const [displayDate, setDisplayDate] = useState(new Date(2000, 0)); + const colorScheme = Appearance.getColorScheme(); + const [isDark, setIsDark] = useState(colorScheme === 'dark'); const getProfile = async () => { try { @@ -46,7 +59,14 @@ function OnboardingScreen() { setLastName(data.last_name || lastName); setUsername(data.username || username); setPronouns(data.pronouns || pronouns); - setBirthday(new Date(data.birthday) || birthday); + if (data.birthday) { + setBirthday( + new Date(data.birthday).toLocaleDateString('en-US', { + timeZone: 'UTC', + }) || birthday, + ); + setBirthdayExists(true); + } setGender(data.gender || gender); setRaceEthnicity(data.race_ethnicity || raceEthnicity); } @@ -75,7 +95,7 @@ function OnboardingScreen() { ...(gender && { gender }), ...(pronouns && { pronouns }), ...(raceEthnicity && { race_ethnicity: raceEthnicity }), - ...(birthday && { birthday }), + ...(birthday && { birthday: displayDate }), }; // Check if user exists @@ -92,15 +112,25 @@ function OnboardingScreen() { .eq('user_id', session?.user.id) .select('*'); - if (error && error instanceof Error) throw error; + if (error && error instanceof Error) { + if (process.env.NODE_ENV !== 'production') { + throw error; + } + } } else { // Create user if they don't exist const { error } = await supabase.from('profiles').insert(updates); - if (error && error instanceof Error) throw error; + if (error && error instanceof Error) { + if (process.env.NODE_ENV !== 'production') { + throw error; + } + } } - Alert.alert('Succesfully updated user!'); + while (router.canGoBack()) { + router.back(); + } router.replace('/home'); } catch (error) { if (error instanceof Error) { @@ -111,74 +141,122 @@ function OnboardingScreen() { } }; + const onConfirmDate = (date: Date) => { + setShowDatePicker(false); + setBirthday(date.toLocaleDateString()); + setDisplayDate(date); + setBirthdayExists(true); + }; + if (!session) { return ; } return ( - - - Welcome, {user?.user_metadata.username} - - - Input your profile information below. - - - - - This information is only used for outreach efforts, and will not be - visible to other users on the app. - - - - - - {showDatePicker && ( - { - setShowDatePicker(Platform.OS === 'ios'); - if (date.nativeEvent.timestamp) { - setBirthday(new Date(date.nativeEvent.timestamp)); - } - }} - /> - )} - - router.replace('/home')} - disabled={false} - /> - + + + + setShowDatePicker(false)} + date={displayDate} + display="inline" + isDarkModeEnabled={isDark} + themeVariant={isDark ? 'dark' : 'light'} + /> + + Welcome, {user?.user_metadata.username} + + + Input your profile information below. + + + + + This information is only used for outreach efforts, and will not + be visible to other users on the app. + + + + + { + setShowDatePicker(!showDatePicker); + }} + > + + + + + + + + + + + + + + + + + + + Skip For Now + + + + ); } diff --git a/src/app/auth/onboarding/styles.tsx b/src/app/auth/onboarding/styles.tsx index a868ff3b..6555413a 100644 --- a/src/app/auth/onboarding/styles.tsx +++ b/src/app/auth/onboarding/styles.tsx @@ -4,25 +4,42 @@ import colors from '../../../styles/colors'; export default StyleSheet.create({ container: { - paddingVertical: 63, + backgroundColor: 'white', + flex: 1, + }, + flex: { + flexGrow: 1, + justifyContent: 'space-between', + paddingTop: 64, + paddingBottom: 54, paddingLeft: 43, paddingRight: 44, }, + inputContainer: { + flex: 1, + gap: 16, + }, subtext: { color: colors.darkGrey, marginLeft: 8, }, - h1: { - marginTop: 66, - }, body1: { marginTop: 26, }, info: { - flex: 1, flexDirection: 'row', marginTop: 12, - marginBottom: 16, width: 250, }, + updateProfileButton: { + marginBottom: 24, + }, + skipButton: { + flex: 1, + alignSelf: 'center', + color: colors.darkGrey, + }, + icon: { + paddingLeft: 8, + }, }); diff --git a/src/app/auth/resetPassword/index.tsx b/src/app/auth/resetPassword/index.tsx index 2356d256..19a193bd 100644 --- a/src/app/auth/resetPassword/index.tsx +++ b/src/app/auth/resetPassword/index.tsx @@ -1,20 +1,19 @@ -import { Link, router } from 'expo-router'; -import React, { useEffect, useRef, useState } from 'react'; +import { router } from 'expo-router'; +import { useEffect, useState } from 'react'; import { Alert, Text, View } from 'react-native'; import { Icon as RNEIcon } from 'react-native-elements'; import styles from './styles'; -import Icon from '../../../../assets/icons'; import StyledButton from '../../../components/StyledButton/StyledButton'; import UserStringInput from '../../../components/UserStringInput/UserStringInput'; import colors from '../../../styles/colors'; import globalStyles from '../../../styles/globalStyles'; import { useSession } from '../../../utils/AuthContext'; import PasswordComplexityText from '../../../components/PasswordComplexityText/PasswordComplexityText'; -import supabase from '../../../utils/supabase'; +import { isPasswordSameAsBefore } from '../../../queries/profiles'; function ResetPasswordScreen() { - const { updateUser, signOut } = useSession(); + const { session, updateUser, signOut } = useSession(); const [password, setPassword] = useState(''); const [passwordTextHidden, setPasswordTextHidden] = useState(true); const [confirmPassword, setConfirmPassword] = useState(''); @@ -38,11 +37,19 @@ function ResetPasswordScreen() { const checkPassword = (text: string) => { if (text !== '') { + isPasswordSameAsBefore(text, session?.user?.id).then(isSame => + setIsDifferent(!isSame), + ); setHasUppercase(text !== text.toLowerCase()); setHasLowercase(text !== text.toUpperCase()); setHasNumber(/[0-9]/.test(text)); setHasLength(text.length >= 8); //need to check that it is different from old password + } else { + setHasUppercase(false); + setHasLowercase(false); + setHasNumber(false); + setHasLength(false); } }; @@ -67,6 +74,7 @@ function ResetPasswordScreen() { const { error } = await updateUser({ password }); if (error) { + console.error(error); Alert.alert('Updating password failed'); } else { await signOut(); @@ -105,38 +113,31 @@ function ResetPasswordScreen() { - {password !== '' && ( - - )} - {password !== '' && ( - - )} - {password !== '' && ( - - )} - {password !== '' && ( - - )} + + + + + + + {/* functionality for this has not been implemented */} - {password !== '' && ( - - )} + {passwordIsValid && ( diff --git a/src/app/auth/signup/index.tsx b/src/app/auth/signup/index.tsx index 41246d2c..fbaeb5cd 100644 --- a/src/app/auth/signup/index.tsx +++ b/src/app/auth/signup/index.tsx @@ -7,7 +7,6 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import validator from 'validator'; import styles from './styles'; -import Icon from '../../../../assets/icons'; import StyledButton from '../../../components/StyledButton/StyledButton'; import UserStringInput from '../../../components/UserStringInput/UserStringInput'; import PasswordComplexityText from '../../../components/PasswordComplexityText/PasswordComplexityText'; @@ -130,7 +129,11 @@ function SignUpScreen() { }); if (error) Alert.alert(error.message); - else router.replace('/auth/verify'); + else + router.replace({ + pathname: '/auth/verify', + params: { finalRedirect: 'onboarding' }, + }); setLoading(false); }; @@ -251,18 +254,20 @@ function SignUpScreen() { loading || emailError !== '' || usernameError !== '' || + firstName.length === 0 || + lastName.length === 0 || email.length === 0 || username.length === 0 } onPress={signUpWithEmail} /> - - Already have an account?{' '} + + Already have an account? Log In - + diff --git a/src/app/auth/signup/styles.tsx b/src/app/auth/signup/styles.tsx index 600bcb26..51de2d40 100644 --- a/src/app/auth/signup/styles.tsx +++ b/src/app/auth/signup/styles.tsx @@ -14,9 +14,11 @@ export default StyleSheet.create({ textDecorationLine: 'underline', }, redirectText: { - textAlign: 'center', - marginBottom: 64, + gap: 8, + flexDirection: 'row', + justifyContent: 'center', marginTop: 16, + marginBottom: 64, }, title: { paddingTop: 64, diff --git a/src/app/auth/verify/index.tsx b/src/app/auth/verify/index.tsx index c469ba84..a3b9406c 100644 --- a/src/app/auth/verify/index.tsx +++ b/src/app/auth/verify/index.tsx @@ -1,4 +1,4 @@ -import { Link, router } from 'expo-router'; +import { Link, Redirect, router, useLocalSearchParams } from 'expo-router'; import { useState, useRef, useEffect } from 'react'; import { View, Text } from 'react-native'; import OTPTextInput from 'react-native-otp-textinput'; @@ -16,14 +16,17 @@ function VerificationScreen() { const [errorMessage, setErrorMessage] = useState(''); const [showX, setShowX] = useState(false); const [userInput, setUserInput] = useState(''); + const params = useLocalSearchParams<{ + finalRedirect: string; + userEmail: string; + }>(); + const { finalRedirect, userEmail } = params; + const email = user?.email ?? userEmail ?? ''; const inputRef = useRef(null); useEffect(() => { if (userInput.length === 6) { - console.log('we are checking'); - console.log(userInput); - verifyCode(); } }, [userInput]); @@ -33,57 +36,54 @@ function VerificationScreen() { }; const verifyCode = async () => { - if (user?.email) { - const { error } = await verifyOtp(user.email, userInput); - - console.log(error); - if (error) { - setShowX(true); - setErrorMessage('Incorrect code. Please try again.'); - } else { - router.replace('/auth/onboarding'); - } + const { error } = await verifyOtp(email, userInput); + + console.log(error); + if (error) { + setShowX(true); + setErrorMessage('Incorrect code. Please try again.'); + } else { + router.replace('/auth/' + finalRedirect); } }; const resendCode = async () => { clearText(); - if (user?.email) { - const { error } = await resendVerification(user.email); - - if (error) { - setShowX(false); - setErrorMessage( - 'Please wait 1 minute for us to resend the verification code.', - ); - } else { - Toast.show({ - type: 'success', - text1: `A new verification code has been sent to`, - text2: `${blurrEmail()}.`, - text2Style: globalStyles.subtextBold, - text1Style: globalStyles.subtext, - position: 'bottom', - }); - } + const { error } = await resendVerification(email); + + if (error) { + setShowX(false); + setErrorMessage( + 'Please wait 1 minute for us to resend the verification code.', + ); + } else { + Toast.show({ + type: 'success', + text1: `A new verification code has been sent to`, + text2: `${blurrEmail()}.`, + text2Style: globalStyles.subtextBold, + text1Style: globalStyles.subtext, + position: 'bottom', + }); } }; const blurrEmail = () => { - if (user?.email) { - const length = user?.email?.split('@')[0].length; - return `${user?.email?.substring(0, 2)}*****${user?.email - ?.split('@')[0] - .substring(length - 1, length)}@${user?.email?.split('@')[1]}`; - } - return 'your email'; + const length = email?.split('@')[0].length; + return `${email?.substring(0, 2)}*****${email + ?.split('@')[0] + .substring(length - 1, length)}@${email?.split('@')[1]}`; }; const renderBlurredEmail = () => { return {blurrEmail()}.; }; + if (email === '') { + return ; + } + return ( diff --git a/src/app/settings/index.tsx b/src/app/settings/index.tsx deleted file mode 100644 index 599c0f3e..00000000 --- a/src/app/settings/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import DateTimePicker from '@react-native-community/datetimepicker'; -import { Redirect, router, Link } from 'expo-router'; -import { useEffect, useState } from 'react'; -import { Text, View, Alert, Platform } from 'react-native'; -import { Button } from 'react-native-elements'; -import { ScrollView } from 'react-native-gesture-handler'; -import { SafeAreaView } from 'react-native-safe-area-context'; - -import styles from './styles'; -import AccountDataDisplay from '../../components/AccountDataDisplay/AccountDataDisplay'; -import StyledButton from '../../components/StyledButton/StyledButton'; -import UserSelectorInput from '../../components/UserSelectorInput/UserSelectorInput'; -import globalStyles from '../../styles/globalStyles'; -import { useSession } from '../../utils/AuthContext'; -import supabase from '../../utils/supabase'; - -function SettingsScreen() { - const { session, signOut } = useSession(); - const [loading, setLoading] = useState(true); - const [firstName, setFirstName] = useState(''); - const [username, setUsername] = useState(''); - const [lastName, setLastName] = useState(''); - const [pronouns, setPronouns] = useState(''); - const [birthday, setBirthday] = useState(new Date()); - const [gender, setGender] = useState(''); - const [raceEthnicity, setRaceEthnicity] = useState(''); - - const [showSaveEdits, setShowSaveEdits] = useState(false); - const [showDatePicker, setShowDatePicker] = useState(Platform.OS === 'ios'); - - const wrapInDetectChange = (onChange: (_: any) => any) => { - return (value: any) => { - setShowSaveEdits(true); - return onChange(value); - }; - }; - - const getProfile = async () => { - try { - setLoading(true); - if (!session?.user) throw new Error('No user on the session!'); - - const { data, error, status } = await supabase - .from('profiles') - .select( - `first_name, last_name, username, birthday, gender, race_ethnicity`, - ) - .eq('user_id', session?.user.id) - .single(); - - if (error && status !== 406 && error instanceof Error) { - throw error; - } - - if (data) { - setFirstName(data.first_name || firstName); - setLastName(data.last_name || lastName); - setUsername(data.username || username); - - if (data.birthday) { - setBirthday(new Date(data.birthday)); - setShowDatePicker(false); - } else { - setShowDatePicker(true); - } - - setGender(data.gender || gender); - // setPronouns(data.pronouns || pronouns); - setRaceEthnicity(data.race_ethnicity || raceEthnicity); - } - } catch (error) { - if (error instanceof Error) { - Alert.alert(`Get profile error: ${error.message}`); - } - } finally { - setLoading(false); - } - }; - - const resetAndPushToRouter = (path: string) => { - while (router.canGoBack()) { - router.back(); - } - router.replace(path); - }; - - useEffect(() => { - if (session) getProfile(); - }, [session]); - - useEffect(() => { - if (!session) resetAndPushToRouter('/auth/login'); - }, [session]); - - const updateProfile = async () => { - try { - setLoading(true); - if (!session?.user) throw new Error('No user on the session!'); - - // Only update values that are not blank - const updates = { - ...(firstName && { first_name: firstName }), - ...(lastName && { last_name: lastName }), - ...(gender && { gender }), - ...(pronouns && { pronouns }), - ...(raceEthnicity && { race_ethnicity: raceEthnicity }), - ...(birthday && { birthday }), - }; - - // Check if user exists - const { count } = await supabase - .from('profiles') - .select(`*`, { count: 'exact' }) - .eq('user_id', session?.user.id); - - if (count && count >= 1) { - // Update user if they exist - const { error } = await supabase - .from('profiles') - .update(updates) - .eq('user_id', session?.user.id) - .select('*'); - - if (error && error instanceof Error) throw error; - } else { - // Create user if they don't exist - const { error } = await supabase.from('profiles').insert(updates); - - if (error && error instanceof Error) throw error; - } - } catch (error) { - if (error instanceof Error) { - Alert.alert(error.message); - } - } finally { - setLoading(false); - setShowSaveEdits(false); - } - }; - - if (!session) { - return ; - } - - return ( - - - {' - - - - - Settings - Account - - - - - - - { - setShowDatePicker(Platform.OS === 'ios'); - if (date.nativeEvent.timestamp) { - setBirthday(new Date(date.nativeEvent.timestamp)); - } - }} - /> - {Platform.OS !== 'ios' && ( - - )} - {showDatePicker && ( - { - setShowDatePicker(Platform.OS === 'ios'); - if (date.nativeEvent.timestamp) { - setDate(new Date(date.nativeEvent.timestamp)); - } - }} - /> - )} - - ); -} - -export default DatePicker; diff --git a/src/components/DatePicker/styles.tsx b/src/components/DatePicker/styles.tsx deleted file mode 100644 index a11509d5..00000000 --- a/src/components/DatePicker/styles.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { StyleSheet } from 'react-native'; - -export default StyleSheet.create({ - mt16: { - marginTop: 16, - width: '100%', - }, - container: { - paddingRight: 10, - marginTop: 8, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderRadius: 5, - borderColor: 'black', - fontFamily: 'Manrope-Regular', - }, - inputField: { - flex: 1, - fontSize: 14, - padding: 10, - color: '#000000', - }, - button: { - backgroundColor: 'gray', - }, - verticallySpaced: { - alignSelf: 'stretch', - }, -}); diff --git a/src/components/GenreCard/GenreCard.tsx b/src/components/GenreCard/GenreCard.tsx index 86b9f552..37bb5360 100644 --- a/src/components/GenreCard/GenreCard.tsx +++ b/src/components/GenreCard/GenreCard.tsx @@ -9,6 +9,7 @@ import styles from './styles'; type GenreCardProps = { subgenres: string; + subgenre_id: number; cardColor: string; pressFunction: (event: GestureResponderEvent) => void; }; diff --git a/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx new file mode 100644 index 00000000..00b413da --- /dev/null +++ b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx @@ -0,0 +1,90 @@ +import { + GestureResponderEvent, + Text, + Image, + View, + TouchableOpacity, +} from 'react-native'; + +import styles from './styles'; +import globalStyles from '../../styles/globalStyles'; + +type GenreStoryPreviewCardProps = { + topic: string[]; + tone: string[]; + genreMedium: string[]; + allTags: string[]; + authorName: string; + storyImage: string; + authorImage: string; + storyTitle: string; + excerpt: { html: string }; + pressFunction: (event: GestureResponderEvent) => void; +}; + +function GenreStoryPreviewCard({ + topic, + tone, + genreMedium, + allTags, + authorName, + storyImage, + authorImage, + storyTitle, + excerpt, + pressFunction, +}: GenreStoryPreviewCardProps) { + return ( + + + + + + {storyTitle} + + + + + + + + + {authorName} + + + + {excerpt.html.slice(3, -3)} + + + + + + + + {genreMedium[0]} + + + {genreMedium[1]} + + + + + + {allTags.length} more{' '} + {allTags.length === 1 ? 'tag' : 'tags'} + + + + + + + + ); +} + +export default GenreStoryPreviewCard; diff --git a/src/components/GenreStoryPreviewCard/styles.ts b/src/components/GenreStoryPreviewCard/styles.ts new file mode 100644 index 00000000..a40bb58e --- /dev/null +++ b/src/components/GenreStoryPreviewCard/styles.ts @@ -0,0 +1,121 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + card: { + flexDirection: 'column', + justifyContent: 'flex-end', + backgroundColor: 'white', + borderRadius: 6, + marginTop: 8, + marginBottom: 8, + shadowColor: 'black', + shadowOffset: { width: 1, height: 3 }, + shadowOpacity: 0.5, + elevation: 10, + paddingRight: 30, + marginRight: 30, + width: '98%', + marginHorizontal: '0.75%', + }, + top: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + bottom: { + flex: 1, + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + height: 48, + backgroundColor: colors.lightGrey, + overflow: 'hidden', + borderRadius: 6, + paddingHorizontal: 12, + paddingTop: 8, + }, + image: { + height: 106, + width: 106, + backgroundColor: colors.lilac, + borderRadius: 4, + marginBottom: 12, + marginTop: 12, + }, + author: { + marginLeft: 8, + }, + authorImage: { + height: 22, + width: 22, + backgroundColor: colors.gwnOrange, + borderRadius: 22 / 2, + }, + cardTextContainer: { + flex: 1, + marginLeft: 16, + marginTop: 12, + marginBottom: 8, + }, + authorContainer: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 10, + }, + title: { + marginBottom: 8, + fontSize: 22, + }, + tags: { + paddingHorizontal: 8, + paddingVertical: 4, + backgroundColor: '#EBEBEB', + borderRadius: 10, + width: 'auto', + marginRight: 8, + marginBottom: 10, + }, + tagsContainer: { + flexDirection: 'row', + justifyContent: 'flex-start', + + alignItems: 'center', + flexWrap: 'wrap', + }, + horizontalLine: { + borderBottomColor: '#EBEBEB', + borderBottomWidth: 1, + marginTop: 5, + marginBottom: 10, + }, + cardContainer2: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, + authorandText: { + flex: 1, + flexDirection: 'column', + marginLeft: 10, + marginTop: -10, + }, + subtext: { + color: '#797979', + fontSize: 15, + }, + tagSubtext: { + color: '#797979', + }, + tagParent: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + }, +}); + +export default styles; diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx index ac894d63..71d6d057 100644 --- a/src/components/PreviewCard/PreviewCard.tsx +++ b/src/components/PreviewCard/PreviewCard.tsx @@ -1,18 +1,25 @@ +import * as cheerio from 'cheerio'; import { GestureResponderEvent, - Image, Pressable, Text, + TouchableOpacity, View, } from 'react-native'; -import * as cheerio from 'cheerio'; +import Emoji from 'react-native-emoji'; +import { Image } from 'expo-image'; import styles from './styles'; import globalStyles from '../../styles/globalStyles'; +import SaveStoryButton from '../SaveStoryButton/SaveStoryButton'; + +const placeholderImage = + 'https://gwn-uploads.s3.amazonaws.com/wp-content/uploads/2021/10/10120952/Girls-Write-Now-logo-avatar.png'; type PreviewCardProps = { title: string; image: string; + storyId: number; author: string; authorImage: string; excerpt: { html: string }; @@ -23,6 +30,7 @@ type PreviewCardProps = { function PreviewCard({ title, image, + storyId, author, authorImage, excerpt, @@ -36,9 +44,15 @@ function PreviewCard({ {title} + + + - + @@ -53,23 +67,40 @@ function PreviewCard({ numberOfLines={3} style={[globalStyles.subtext, styles.storyDescription]} > - "{cheerio.load(excerpt.html).text()}" + "{cheerio.load(excerpt.html ?? '').text()}" - - - - {tags[0]} + + + + + + + + + + + {/* heart, clap, muscle, cry, ??? */} + + + 14{/*change number to work*/} - - + + {(tags?.length ?? 0) > 0 && ( + + + {tags[0]} + + + )} + {' '} - + {tags.length - 1} more tags + + {(tags?.length ?? 1) - 1} diff --git a/src/components/PreviewCard/styles.ts b/src/components/PreviewCard/styles.ts index d69aae26..d8d3ef1d 100644 --- a/src/components/PreviewCard/styles.ts +++ b/src/components/PreviewCard/styles.ts @@ -11,8 +11,8 @@ const styles = StyleSheet.create({ marginTop: 8, marginBottom: 8, shadowColor: 'black', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.5, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.2, elevation: 4, }, image: { @@ -49,8 +49,13 @@ const styles = StyleSheet.create({ titleContainer: { paddingTop: 16, paddingLeft: 12, + paddingRight: 12, + paddingBottom: 8, borderBottomColor: '#EBEBEB', borderBottomWidth: StyleSheet.hairlineWidth, + flexDirection: 'row', + flexGrow: 1, + justifyContent: 'space-between', }, tag: { paddingHorizontal: 8, @@ -62,7 +67,6 @@ const styles = StyleSheet.create({ marginBottom: 10, }, tagsContainer: { - // backgroundColor: colors.darkGrey, flex: 1, flexDirection: 'row', justifyContent: 'space-between', @@ -75,19 +79,40 @@ const styles = StyleSheet.create({ }, tagsRow: { flexDirection: 'row', - justifyContent: 'flex-start', + justifyContent: 'flex-end', + alignContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', - paddingTop: 4, }, moreTags: { + paddingVertical: 10, paddingRight: 12, alignItems: 'center', - justifyContent: 'center', + justifyContent: 'flex-end', }, moreTagsText: { color: colors.darkGrey, }, + reactions: { + width: 32, + height: 32, + borderRadius: 32 / 2, + borderWidth: 1, + backgroundColor: '#89CFF0', //different per emoji reaction + borderColor: 'white', + marginTop: 10, + marginRight: -5, // -10 + overflow: 'hidden', + justifyContent: 'center', + paddingLeft: 4, + }, + reactionText: { + color: colors.grey, + }, + reactionNumber: { + marginLeft: 16, + marginTop: 16, + }, storyDescription: { color: colors.darkGrey, paddingRight: 12, diff --git a/src/components/RecentSearchCard/RecentSearchCard.tsx b/src/components/RecentSearchCard/RecentSearchCard.tsx index eb77fcc6..e142baee 100644 --- a/src/components/RecentSearchCard/RecentSearchCard.tsx +++ b/src/components/RecentSearchCard/RecentSearchCard.tsx @@ -21,10 +21,14 @@ function RecentSearchCard({ - {value} + + {value} + - {numResults} Results + + {numResults} Results + diff --git a/src/components/RecentSearchCard/styles.ts b/src/components/RecentSearchCard/styles.ts index 676937de..b0cb9012 100644 --- a/src/components/RecentSearchCard/styles.ts +++ b/src/components/RecentSearchCard/styles.ts @@ -1,4 +1,5 @@ import { StyleSheet } from 'react-native'; +import colors from '../../styles/colors'; const styles = StyleSheet.create({ card: { @@ -13,10 +14,11 @@ const styles = StyleSheet.create({ paddingRight: 12, paddingBottom: 10, paddingTop: 10, + shadowColor: 'black', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.5, - elevation: 4, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.3, + elevation: 7, }, leftItems: { gap: 8, @@ -29,9 +31,6 @@ const styles = StyleSheet.create({ alignItems: 'center', }, searchValueText: { - color: 'black', - fontWeight: '400', - fontSize: 14, justifyContent: 'center', }, numResultsText: { diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx new file mode 100644 index 00000000..c4d43315 --- /dev/null +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { + addUserStoryToReadingList, + deleteUserStoryToReadingList, + isStoryInReadingList, +} from '../../queries/savedStories'; +import { usePubSub } from '../../utils/PubSubContext'; +import { useSession } from '../../utils/AuthContext'; +import { Image } from 'expo-image'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +type SaveStoryButtonProps = { + storyId: number; +}; + +const saveStoryImage = require('../../../assets/save_story.png'); +const savedStoryImage = require('../../../assets/saved_story.png'); + +export default function SaveStoryButton({ storyId }: SaveStoryButtonProps) { + const { user } = useSession(); + const [storyIsSaved, setStoryIsSaved] = useState(false); + const { channels, initializeChannel, publish } = usePubSub(); + + useEffect(() => { + isStoryInReadingList(storyId, user?.id).then(storyInReadingList => { + setStoryIsSaved(storyInReadingList); + initializeChannel(storyId); + }); + }, [storyId]); + + useEffect(() => { + // if another card updates this story, update it here also + if (typeof channels[storyId] !== 'undefined') { + setStoryIsSaved(channels[storyId] ?? false); + } + }, [channels[storyId]]); + + useEffect(() => { + isStoryInReadingList(storyId, user?.id).then(storyInReadingList => + setStoryIsSaved(storyInReadingList), + ); + }, [storyId]); + + const saveStory = async (saved: boolean) => { + setStoryIsSaved(saved); + publish(storyId, saved); // update other cards with this story + + if (saved) { + await addUserStoryToReadingList(user?.id, storyId); + } else { + await deleteUserStoryToReadingList(user?.id, storyId); + } + }; + + return ( + saveStory(!storyIsSaved)}> + {storyIsSaved ? ( + + ) : ( + + )} + + ); +} diff --git a/src/components/StyledButton/styles.tsx b/src/components/StyledButton/styles.tsx index 4e664ced..b2c4dc94 100644 --- a/src/components/StyledButton/styles.tsx +++ b/src/components/StyledButton/styles.tsx @@ -13,7 +13,7 @@ export default StyleSheet.create({ }, titleStyle: { paddingHorizontal: 24, - paddingVertical: 10, + paddingVertical: 5, color: 'white', }, }); diff --git a/src/components/UserSelectorInput/UserSelectorInput.tsx b/src/components/UserSelectorInput/UserSelectorInput.tsx index dd5be4fc..a032b1ab 100644 --- a/src/components/UserSelectorInput/UserSelectorInput.tsx +++ b/src/components/UserSelectorInput/UserSelectorInput.tsx @@ -24,7 +24,7 @@ function UserSelectorInput({ value, }: UserSelectorInputProps) { return ( - + {label} ( diff --git a/src/components/UserSelectorInput/styles.tsx b/src/components/UserSelectorInput/styles.tsx index ad54c0ab..734f782b 100644 --- a/src/components/UserSelectorInput/styles.tsx +++ b/src/components/UserSelectorInput/styles.tsx @@ -10,17 +10,15 @@ export default StyleSheet.create({ position: 'relative', zIndex: 1, }, - container: { - marginBottom: 16, - }, label: { - marginBottom: 10, + marginBottom: 8, }, dropdown: { - height: 50, + height: 44, borderWidth: 1, borderRadius: 5, paddingHorizontal: 10, + paddingVertical: 10, }, dropdownContainer: { borderRadius: 5, diff --git a/src/components/UserStringInput/UserStringInput.tsx b/src/components/UserStringInput/UserStringInput.tsx index f7cd12d6..6d2d586b 100644 --- a/src/components/UserStringInput/UserStringInput.tsx +++ b/src/components/UserStringInput/UserStringInput.tsx @@ -2,8 +2,8 @@ import { ReactNode } from 'react'; import { View, Text, TextInput } from 'react-native'; import styles from './styles'; -import globalStyles from '../../styles/globalStyles'; import colors from '../../styles/colors'; +import globalStyles from '../../styles/globalStyles'; type UserStringInputProps = { placeholder: string; @@ -23,7 +23,7 @@ export default function UserStringInput({ label, children, labelColor = '#000', - placeholderTextColor = '#000', + placeholderTextColor = colors.darkGrey, onChange = _ => {}, }: UserStringInputProps) { return ( diff --git a/src/components/UserStringInput/styles.tsx b/src/components/UserStringInput/styles.tsx index a11509d5..5642a55e 100644 --- a/src/components/UserStringInput/styles.tsx +++ b/src/components/UserStringInput/styles.tsx @@ -14,7 +14,6 @@ export default StyleSheet.create({ borderWidth: 1, borderRadius: 5, borderColor: 'black', - fontFamily: 'Manrope-Regular', }, inputField: { flex: 1, diff --git a/src/queries/genres.tsx b/src/queries/genres.tsx index 4710cbcd..ca648b71 100644 --- a/src/queries/genres.tsx +++ b/src/queries/genres.tsx @@ -1,5 +1,4 @@ -// eslint-disable-next-line import/namespace -import { Genre } from './types'; +import { Genre, GenreStories } from './types'; import supabase from '../utils/supabase'; export async function fetchGenres(): Promise { @@ -12,3 +11,21 @@ export async function fetchGenres(): Promise { return data; } } + +export async function fetchGenreStoryById( + parent_id: number, +): Promise { + const { data, error } = await supabase.rpc( + 'fetch_genre_and_subgenre_stories', + { + genre_parent_id: parent_id, + }, + ); + if (error) { + throw new Error( + `An error occured when trying to fetch all genres story previews ${error}`, + ); + } else { + return data; + } +} diff --git a/src/queries/profiles.tsx b/src/queries/profiles.tsx index 7bc979aa..dfdaea4c 100644 --- a/src/queries/profiles.tsx +++ b/src/queries/profiles.tsx @@ -25,3 +25,20 @@ export async function isEmailTaken(newEmail: string) { const emailIsTaken = (count ?? 0) >= 1; return emailIsTaken as boolean; } + +export async function isPasswordSameAsBefore( + new_plain_password: string, + user_id: string | undefined, +): Promise { + let { data, error } = await supabase.rpc('check_same_as_old_pass', { + new_plain_password, + user_id, + }); + + if (error) { + console.error(error); + return false; + } else { + return data; + } +} diff --git a/src/queries/reactions.tsx b/src/queries/reactions.tsx new file mode 100644 index 00000000..ffe68c40 --- /dev/null +++ b/src/queries/reactions.tsx @@ -0,0 +1,79 @@ +import { Reactions } from './types'; +import supabase from '../utils/supabase'; + +export async function addReactionToStory( + input_profile_id: number, + input_story_id: number, + input_reaction_id: number, +): Promise { + const { data, error } = await supabase.rpc('add_reaction_to_story', { + story_id: input_story_id, + profile_id: input_profile_id, + reaction_id: input_reaction_id, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to insert author reaction to story: ${error}`, + ); + } else { + return data; + } +} + +export async function deleteReactionToStory( + input_profile_id: number, + input_story_id: number, + input_reaction_id: number, +): Promise { + const { data, error } = await supabase.rpc('remove_reaction_from_story', { + story_id: input_story_id, + profile_id: input_profile_id, + reaction_id: input_reaction_id, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to delete reaction to story by a user: ${error}`, + ); + } else { + return data; + } +} + +export async function fetchAllReactionsToStory( + storyId: number, +): Promise { + const { data, error } = await supabase.rpc('curr_get_reactions_for_story', { + input_story_id: storyId, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to fetch reactions to a story', ${error}`, + ); + } else { + return data as Reactions[]; + } +} + +export async function fetchReactionsToStoryByUser( + story_id: number, + profile_id: number, +): Promise { + const { data, error } = await supabase.rpc( + 'get_reactions_for_user_and_story', + { + _story_id: story_id, + _profile_id: profile_id, + }, + ); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to fetch reactions for user and story ${error}`, + ); + } else { + return data; + } +} diff --git a/src/queries/savedStories.tsx b/src/queries/savedStories.tsx index 59f6ecaa..0416ecb8 100644 --- a/src/queries/savedStories.tsx +++ b/src/queries/savedStories.tsx @@ -1,7 +1,9 @@ import supabase from '../utils/supabase'; -const favorites = 'favorites'; -const readingList = 'reading list'; +enum SavedList { + FAVORITES = 'favorites', + READING_LIST = 'reading list', +} async function fetchUserStories( user_id: string | undefined, @@ -48,11 +50,11 @@ async function fetchUserStories( } export async function fetchUserStoriesFavorites(user_id: string | undefined) { - return await fetchUserStories(user_id, favorites); + return await fetchUserStories(user_id, SavedList.FAVORITES); } export async function fetchUserStoriesReadingList(user_id: string | undefined) { - return await fetchUserStories(user_id, readingList); + return await fetchUserStories(user_id, SavedList.READING_LIST); } async function addUserStory( @@ -62,13 +64,15 @@ async function addUserStory( ) { const { error } = await supabase .from('saved_stories') - .insert([{ user_id: user_id, story_id: story_id, name: name }]) + .upsert([{ user_id: user_id, story_id: story_id, name: name }]) .select(); if (error) { if (process.env.NODE_ENV !== 'production') { throw new Error( - `An error occured when trying to set user saved stories: ${error.details}`, + `An error occured when trying to set user saved stories: ${JSON.stringify( + error, + )}`, ); } } @@ -78,17 +82,31 @@ export async function addUserStoryToFavorites( user_id: string | undefined, story_id: number, ) { - addUserStory(user_id, story_id, favorites); + addUserStory(user_id, story_id, SavedList.FAVORITES); } export async function addUserStoryToReadingList( user_id: string | undefined, story_id: number, ) { - addUserStory(user_id, story_id, readingList); + addUserStory(user_id, story_id, SavedList.READING_LIST); +} + +export async function deleteUserStoryToFavorites( + user_id: string | undefined, + story_id: number, +) { + deleteUserStory(user_id, story_id, SavedList.FAVORITES); } -export async function deleteUserStories( +export async function deleteUserStoryToReadingList( + user_id: string | undefined, + story_id: number, +) { + deleteUserStory(user_id, story_id, SavedList.READING_LIST); +} + +export async function deleteUserStory( user_id: string | undefined, story_id: number, name: string, @@ -108,3 +126,21 @@ export async function deleteUserStories( } } } + +export async function isStoryInReadingList( + storyId: number, + userId: string | undefined, +): Promise { + let { data, error } = await supabase.rpc('is_story_saved_for_user', { + list_name: 'reading list', + story_db_id: storyId, + user_uuid: userId, + }); + + if (error) { + console.error(error); + return false; + } + + return data; +} diff --git a/src/queries/stories.tsx b/src/queries/stories.tsx index 68bfafc4..9b8e833b 100644 --- a/src/queries/stories.tsx +++ b/src/queries/stories.tsx @@ -25,7 +25,6 @@ export async function fetchStory(storyId: number): Promise { `An error occured when trying to fetch story ${storyId}: ${error.code}`, ); } else { - console.log(data); return data; } } @@ -83,3 +82,35 @@ export async function fetchNewStories(): Promise { return data; } } + +export async function fetchStoryPreviewById( + storyId: number, +): Promise { + const { data, error } = await supabase.rpc('curr_story_preview_by_id', { + input_story_id: storyId, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to fetch story preview by ID: ${error}`, + ); + } else { + return data; + } +} + +export async function fetchStoryPreviewByIds( + storyIds: number[], +): Promise { + const { data, error } = await supabase.rpc('curr_story_preview_by_ids', { + input_ids: storyIds, + }); + if (error) { + console.log(error); + throw new Error( + `An error occured when trying to fetch story preview by IDs: ${error}`, + ); + } else { + return data; + } +} diff --git a/src/queries/types.tsx b/src/queries/types.tsx index 04ec7d19..60748adc 100644 --- a/src/queries/types.tsx +++ b/src/queries/types.tsx @@ -47,6 +47,7 @@ export interface StoryCard { title: string; author_name: string; featured_media: string; + author_image: string; } export interface Subgenre { @@ -59,3 +60,18 @@ export interface Genre { parent_name: string; subgenres: Subgenre[]; } + +export interface GenreStories { + parent_id: number; + parent_name: string; + subgenre_id: number; + subgenre_name: string; + genre_story_previews: string[]; +} + +export interface Reactions { + profile_id: number; + story_id: number; + emoji_id: number; + emoji: string; +} diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 73d10142..af1f15f4 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -14,6 +14,7 @@ const colors = { fadedBlack: '#2D2D2D', white: '#FBFBFB', grey: '#A7A5A5', + grey2: '#D9D9D9', darkGrey: '#797979', textPrimary: '#000000', // black diff --git a/src/styles/globalStyles.ts b/src/styles/globalStyles.ts index ef9e2cb3..3a104cf0 100644 --- a/src/styles/globalStyles.ts +++ b/src/styles/globalStyles.ts @@ -10,6 +10,15 @@ export default StyleSheet.create({ justifyContent: 'flex-start', paddingHorizontal: 24, }, + + tabBarContainer: { + flex: 1, + backgroundColor: 'white', + alignItems: 'flex-start', + justifyContent: 'flex-start', + paddingHorizontal: 24, + paddingBottom: 60, + }, authContainer: { marginHorizontal: 38, flex: 1, @@ -79,6 +88,12 @@ export default StyleSheet.create({ textAlign: 'left', color: 'black', }, + body2Bold: { + fontFamily: 'Manrope-Bold', + fontSize: 16, + textAlign: 'left', + color: 'black', + }, body3: { fontFamily: 'Manrope-Regular', fontSize: 18, @@ -135,3 +150,5 @@ export default StyleSheet.create({ marginTop: 20, }, }); + +export const fonts = ['Manrope-Bold', 'Manrope-Regular', 'Manrope-Semibold']; diff --git a/src/utils/FilterContext.tsx b/src/utils/FilterContext.tsx new file mode 100644 index 00000000..b488e0dd --- /dev/null +++ b/src/utils/FilterContext.tsx @@ -0,0 +1,186 @@ +import React, { + createContext, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; +import supabase from './supabase'; + +type FilterAction = + | { type: 'SET_TAGS'; tags: TagFilter[] } + | { type: 'TOGGLE_FILTER'; id: number } + | { type: 'SET_FILTER'; id: number; value: boolean } + | { type: 'CLEAR_ALL'; category: string } + | { type: 'TOGGLE_MAIN_GENRE'; mainGenreId: number }; + +export type FilterDispatch = React.Dispatch; + +export type TagFilter = { + id: number; + name: string; + category: string; + active: boolean; + parent: number | null; +}; + +type ParentFilter = { children: TagFilter[] } & TagFilter; + +export interface FilterState { + filters: Map; + isLoading: boolean; + dispatch: FilterDispatch; +} + +const FilterContext = createContext({} as FilterState); + +const mapParentsAndChildren = ( + filters: Map, + func: (filter: TagFilter) => TagFilter, +) => { + return new Map( + Array.from(filters).map(([id, parent]) => { + return [ + id, + { + ...func(parent), + children: parent.children.map(func), + } as ParentFilter, + ]; + }), + ); +}; + +export const useFilterReducer = () => + useReducer( + (prevState: FilterState, action: FilterAction) => { + switch (action.type) { + case 'SET_TAGS': + const nestedFilters = new Map(); + action.tags + .filter(filter => filter.parent === null) + .map(parentFilter => { + nestedFilters.set(parentFilter.id, { + ...parentFilter, + children: [], + } as ParentFilter); + }); + + action.tags.map(childFilter => { + if (childFilter.parent) { + nestedFilters.get(childFilter.parent)?.children.push(childFilter); + } + }); + + return { + ...prevState, + filters: nestedFilters, + isLoading: false, + }; + case 'SET_FILTER': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, fitler => + fitler.id == action.id + ? { ...fitler, active: action.value } + : fitler, + ), + }; + case 'TOGGLE_FILTER': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, fitler => + fitler.id == action.id + ? { ...fitler, active: !fitler.active } + : fitler, + ), + }; + case 'CLEAR_ALL': + return { + ...prevState, + filters: mapParentsAndChildren(prevState.filters, filter => + filter.category == action.category + ? { ...filter, active: false } + : filter, + ), + }; + case 'TOGGLE_MAIN_GENRE': + const parentGenre = prevState.filters.get(action.mainGenreId); + const newActiveState = !parentGenre?.active; + + const updatedFilters = mapParentsAndChildren( + prevState.filters, + tag => + tag.parent == action.mainGenreId || tag.id == action.mainGenreId + ? { ...tag, active: newActiveState } + : tag, + ); + + return { + ...prevState, + filters: updatedFilters, + }; + default: + return prevState; + } + }, + { + filters: new Map(), + isLoading: true, + dispatch: () => null, + }, + ); + +export function useFilter() { + const value = useContext(FilterContext); + if (process.env.NODE_ENV !== 'production') { + if (!value) { + throw new Error( + 'useFilter must be wrapped in a ', + ); + } + } + + return value; +} + +export function FilterContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [filterState, dispatch] = useFilterReducer(); + + const getTags = async () => { + const { data } = await supabase.from('tags').select(`*`); + + return data?.map(entry => { + const { category, id, name, parent_id } = entry; + return { + id, + name, + category, + parent: parent_id, + active: false, + } as TagFilter; + }); + }; + + useEffect(() => { + getTags().then(tags => dispatch({ type: 'SET_TAGS', tags: tags ?? [] })); + }, []); + + const filterContextValue = useMemo( + () => ({ + ...filterState, + dispatch, + }), + [filterState], + ); + + return ( + + {children} + + ); +} diff --git a/src/utils/PubSubContext.tsx b/src/utils/PubSubContext.tsx new file mode 100644 index 00000000..fbb9ada6 --- /dev/null +++ b/src/utils/PubSubContext.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; + +export interface PubSubState { + channels: Record; + initializeChannel: (id: number) => void; + publish: (id: number, message: boolean) => void; +} + +const BooleanPubSubContext = createContext({} as PubSubState); + +export function usePubSub() { + const value = useContext(BooleanPubSubContext); + if (process.env.NODE_ENV !== 'production') { + if (!value) { + throw new Error( + 'usePubSub must be wrapped in a ', + ); + } + } + + return value; +} + +export function BooleanPubSubProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [channels, setChannels] = useState>( + {}, + ); + + const initializeChannel = (id: number) => { + if (!(id in channels)) { + setChannels({ ...channels, [id]: undefined }); + } + }; + + const publish = (id: number, message: boolean) => { + setChannels({ ...channels, [id]: message }); + }; + + const authContextValue = useMemo( + () => ({ + channels, + initializeChannel, + publish, + }), + [channels], + ); + + return ( + + {children} + + ); +}