Compare commits

...

62 commits
v6.0.0 ... main

Author SHA1 Message Date
zyachel
2796cd8420
Merge pull request #141 from shindouj/patch-1
Added quetre.jeikobu.net to instance list
2024-11-30 13:46:47 +00:00
Shindou Jeikobu
3b6ad9691b
Added instance to README 2024-11-30 12:56:48 +01:00
Shindou Jeikobu
53c929467a
Added quetre.jeikobu.net to instance list 2024-11-30 02:06:56 +01:00
zyachel
f258719c97 Squashed commit of the following:
commit 536fad2fc6
Author: Butter Cat <ButteredCats@protonmail.com>
Date:   Fri Nov 8 15:43:37 2024 -0500

    Remove catsarch.com from readme

commit 123bf7618b
Author: Butter Cat <ButteredCats@protonmail.com>
Date:   Fri Nov 8 14:38:32 2024 -0500

    Remove quetre.catsarch.com
2024-11-08 21:28:49 +00:00
zyachel
e1601c1ca3 Merge branch 'main' of github:zyachel/quetre 2024-10-26 19:34:51 +00:00
zyachel
5dd94140a4 Squashed commit of the following:
commit 33e6776fa2af9f851ee779fdb85c5b56032cf2c4
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Sat Oct 26 00:38:08 2024 +0000

    Update instances.json

    Removing zorby.top instance as all three connections are down.

commit 302da4bb9ecdb39149a37282198a9e487d767684
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Sat Oct 26 00:36:36 2024 +0000

    Update README.md

    Removing zorby.top instance, as all three connections are down.

commit dc32fdaa9fb30182020521bc1925ab02d5728d8a
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Wed Oct 23 22:01:19 2024 +0000

    Update instances.json

    Removing dead instances. Removing frontendfriendly instance as it is broken.

commit 0adc4292ce00fec4b382583369d3f437fedff04d
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Wed Oct 23 21:59:39 2024 +0000

    Update README.md

    Removing additional dead instances. Removed frontendfriendly as it is currently broken.

commit a6e89b6eb5c5412a1d3ecb82ef98956154fb1caf
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Sun Oct 20 00:35:27 2024 +0000

    Update README.md

    Removing offline instances.

commit a5f09e7f98342f62e889b1752e66f66fbae0ef84
Author: Binzy_Boi <binzy_boi@noreply.codeberg.org>
Date:   Sun Oct 20 00:33:38 2024 +0000

    Update instances.json

    Removing instances that have gone offline.
2024-10-26 19:25:49 +00:00
zyachel
d7d99ccdb1
Merge pull request #137 from GitGitro/new-instance
docs: new instance
2024-10-05 12:49:18 +00:00
GitGitro
28d1a30fcd docs: new instance 2024-10-04 17:47:18 +02:00
zyachel
47733fa2c3 docs(instances): update instances list
add a new instance by @vlnst

closes https://codeberg.org/zyachel/quetre/issues/38

re https://github.com/zyachel/quetre/issues/136#issuecomment-2323528853
2024-10-02 19:53:27 +00:00
zyachel
e22ae77c11 docs(instances): add a new instance
by @hyperdefined

closes https://github.com/zyachel/quetre/issues/136
2024-09-01 19:04:36 +00:00
zyachel
75f899f7fc docs(instances): change operated by url of @Troughy's instance
closes https://github.com/zyachel/quetre/issues/135
2024-08-04 14:45:01 +05:30
zyachel
e58fd34df0 add a new instance by @Troughy
closes https://github.com/zyachel/quetre/issues/133
2024-07-21 15:40:19 +05:30
zyachel
1773b9d916
Merge pull request #134 from kumitterer/patch-1
Add quetre.private.coffee instance
2024-07-07 11:31:41 +00:00
Kumi
15c2a0c07d
Update README.md to include quetre.private.coffee 2024-07-06 16:35:11 +02:00
Kumi
5b6a32c7ba
Update instances.json to include quetre.private.coffee 2024-07-06 16:30:51 +02:00
zyachel
645658b291
Merge pull request #132 from ButteredCats/main
Add catsarch I2P instance
2024-06-10 14:21:28 +00:00
Butter Cat
6fe64de256
Add catsarch I2P instance 2024-06-09 12:41:50 -04:00
Butter Cat
0cd2fc24e3
Add catsarch I2P instance 2024-06-09 12:40:36 -04:00
zyachel
248e6209dc
Merge pull request #130 from Fijxu/main
Add quetre.nadeko.net instance
2024-05-10 13:41:59 +00:00
Fijxu
304fecd927
Add quetre.nadeko.net instance 2024-05-09 19:02:44 -04:00
zyachel
c5dff2a617 fix(theme): fix theme not being applied on a single click 2024-04-18 23:52:37 +05:30
zyachel
69f464d7f7 fix: add disclaimer
resolves https://codeberg.org/zyachel/quetre/issues/31
2024-04-18 23:43:00 +05:30
zyachel
1c4acac99f chore(release): 8.0.0 2024-04-08 00:34:48 +05:30
zyachel
169ceb86ea build(dependencies): upgrade dependencies 2024-04-08 00:33:59 +05:30
zyachel
f49062d44a fix(search): remove broken search route
can't do searches now unless logged-in

BREAKING CHANGE: any search request will be responded with a 410
2024-04-08 00:33:16 +05:30
zyachel
a0ac36a174 fix: remove stats from UI that aren't available anymore
includes no. of topic's followers, no. of topics a person is following, etc.
2024-04-08 00:28:59 +05:30
zyachel
f11d3f2ac6 fix(parse): don't bail out on encountering weird characters
replace all \x3C(<) with its valid unique escape sequence
2024-04-08 00:22:51 +05:30
zyachel
1073e61530 Merge branch 'instances-list-json' of github.com:jamesbrown8532/quetre into jamesbrown8532-instances-list-json 2024-02-18 11:39:18 +05:30
jamesbrown8532
8e94ecbefc
Fix missing country information 2024-02-18 00:59:48 +00:00
jamesbrown8532
b369bd31cd
Mention instances.json in README.md 2024-02-18 00:15:32 +00:00
jamesbrown8532
81eb347c5b
Add instances list in JSON format 2024-02-18 00:13:50 +00:00
jamesbrown8532
339cbbecd4
Fix table in README.md 2024-02-18 00:06:59 +00:00
jamesbrown8532
5410574594
Improve instances table formatting 2024-02-17 23:38:15 +00:00
zyachel
dcd21c4664 Merge branch 'patch-1' of github.com:nyuuzyou/quetre into nyuuzyou-patch-1 2024-01-16 02:02:22 +05:30
zyachel
da85a6f2f9 docs(redirectors): add Predirect to redirectors list
a MV3-based addon by @libreom

closes https://github.com/zyachel/quetre/issues/125
2024-01-16 01:46:17 +05:30
nyuuzyou
a25c5bfa24
Added quetre.ducks.party instance
Uptime: https://uptime.ducks.party/status/ducks
2024-01-15 00:47:22 +03:00
zyachel
ac1007bc61 docs(instances): add a new instance
by @ButteredCats

closes https://github.com/zyachel/quetre/issues/123
2024-01-01 18:25:13 +05:30
zyachel
3889185c4d docs(instances): update instances list
add a new instance by @r2fo

closes https://github.com/zyachel/quetre/issues/122
2023-12-18 01:35:41 +05:30
zyachel
6fb2ff4c71 chore(release): 7.1.0 2023-11-05 15:34:09 +05:30
zyachel
7547f54d70 fix: fix a typo while modifying url 2023-11-05 15:33:26 +05:30
zyachel
c6c4828422 fix(fetcher): fix answers route crashing in case there aren't any answers 2023-11-05 15:32:17 +05:30
zyachel
4cd551438b fix(search): show search query along with results 2023-11-05 15:31:33 +05:30
zyachel
f77a1b15bf feat(answers): mark related questions that are unanswered
prepends a little [unanswered] text to question links that have no answers

closes https://github.com/zyachel/quetre/issues/54

Co-authored-by: thecashewtrader <thecashewtrader@protonmail.com>
2023-11-05 14:55:02 +05:30
Korbs
74c64c00d0
Update link to SudoVanilla Instance 2023-10-28 21:12:22 -04:00
drgns-wtd
2320c6c4d5
feat: add docker support with redis(#118) 2023-10-14 12:03:51 +00:00
zyachel
fff9f9b1aa docs(instances): update instances list
add a new instance from @drgns-wtd

fix: https://github.com/zyachel/quetre/issues/119
2023-10-14 15:49:16 +05:30
Korbs
1e0b0df79d
Add Instance from SudoVanilla 2023-10-03 16:20:24 -04:00
zyachel
b409a5cf72 docs(readme): update instances list
add two new instances thanks to @ButteredCats and @unstablemaple

closes https://github.com/zyachel/quetre/issues/115
closes https://github.com/zyachel/quetre/issues/116
2023-10-03 00:36:48 +05:30
zyachel
fd7da0cba5 docs(readme): update instances list
thanks to @extremelyonline and @halictuz, we have two new instances.
also remove instance from fascinated.cc as it has invalid cert

closes https://github.com/zyachel/quetre/issues/112
closes https://github.com/zyachel/quetre/issues/113
closes https://github.com/zyachel/quetre/issues/114
2023-09-02 22:03:01 +05:30
PrivacyDevel
a85e3dd44d
Update details about privacydev.net instances 2023-07-19 18:31:41 +00:00
zyachel
1ec97a9eb3 docs(instances): update instances list
add a new instance by @toyboatcash, and remove pufe.org's instance

closes https://github.com/zyachel/quetre/issues/109, closes
https://github.com/zyachel/quetre/issues/110
2023-07-09 19:19:45 +05:30
zyachel
a951cd8456 docs(instances): update instances list
remove esmail's dead instance

closes https://github.com/zyachel/quetre/issues/107
2023-06-18 16:43:35 +05:30
NoPlagiarism
50592d6afb Add WhateverItWorks instance 2023-05-26 00:47:01 +05:00
zyachel
886c3c92ad docs(instances): update instances list
add a new instance by @RealFascinated
also remove marcopisco's instance

closes: https://github.com/zyachel/quetre/issues/105
closes: https://github.com/zyachel/quetre/issues/106
2023-05-07 17:33:27 +05:30
zyachel
972579dbdf docs(instances): update instances list
add a new instance by @xbdm

close https://github.com/zyachel/quetre/issues/103
2023-04-26 21:28:41 +05:30
PrivacyDevel
17ba1dbf08
Updated info about quetre.privacydev.net and its hidden service 2023-04-26 00:59:32 +00:00
zyachel
b40cc2557d chore(release): 7.0.0 2023-04-23 19:14:37 +05:30
zyachel
10eea08305 feat(cache): increase ttl for routes that are cached but being accessed again 2023-04-23 19:13:21 +05:30
zyachel
35a0d87c13 fix: fix flash of inaccurate color theme 2023-04-23 19:12:24 +05:30
zyachel
b8a0b9fcad fix(answers): fix answers route crashing
this commit fixes the crash that was caused by change in a couple of properties from upstream

BREAKING CHANGE: old fetcher may or may not work

fix: https://github.com/zyachel/quetre/issues/101
2023-04-23 19:10:42 +05:30
PrivacyDevel
0ebaec7065
Updated info for PrivacyDev's Tor instance 2023-04-01 19:04:27 +00:00
PrivacyDevel
a6f7294e97 Updated details for quetre.privacydev.net 2023-03-18 16:17:04 +05:30
32 changed files with 1336 additions and 1401 deletions

View file

@ -2,6 +2,53 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [8.0.0](https://github.com/zyachel/quetre/compare/v7.1.0...v8.0.0) (2024-04-07)
### ⚠ BREAKING CHANGES
* **search:** any search request will be responded with a 410
### Bug Fixes
* **parse:** don't bail out on encountering weird characters ([f11d3f2](https://github.com/zyachel/quetre/commit/f11d3f2ac641bce64c5a674c0995cf85ffc7e37e))
* remove stats from UI that aren't available anymore ([a0ac36a](https://github.com/zyachel/quetre/commit/a0ac36a174849a466c6b63ff65161e4627f1a56b))
* **search:** remove broken search route ([f49062d](https://github.com/zyachel/quetre/commit/f49062d44ac04c77c3f064e703f40a8e114b5776))
## [7.1.0](https://github.com/zyachel/quetre/compare/v7.0.0...v7.1.0) (2023-11-05)
### Features
* add docker support with redis([#118](https://github.com/zyachel/quetre/issues/118)) ([2320c6c](https://github.com/zyachel/quetre/commit/2320c6c4d5edc77f8c84f3a23fb1b807e7747325))
* **answers:** mark related questions that are unanswered ([f77a1b1](https://github.com/zyachel/quetre/commit/f77a1b15bfbbf395f47c107356942fc5d7186a45))
### Bug Fixes
* **fetcher:** fix answers route crashing in case there aren't any answers ([c6c4828](https://github.com/zyachel/quetre/commit/c6c4828422f3349cab72b76fd32448f1b5ab9b7e))
* fix a typo while modifying url ([7547f54](https://github.com/zyachel/quetre/commit/7547f54d7016a4b9f64b712522c92b8bd6f8ccaf))
* **search:** show search query along with results ([4cd5514](https://github.com/zyachel/quetre/commit/4cd551438b288841e41efea5e95cae02f1c27076))
## [7.0.0](https://github.com/zyachel/quetre/compare/v6.0.0...v7.0.0) (2023-04-23)
### ⚠ BREAKING CHANGES
* **answers:** old fetcher may or may not work
fix: https://github.com/zyachel/quetre/issues/101
### Features
* **cache:** increase ttl for routes that are cached but being accessed again ([10eea08](https://github.com/zyachel/quetre/commit/10eea0830511d0e914a255a05904ab18265e46e6))
### Bug Fixes
* **answers:** fix answers route crashing ([b8a0b9f](https://github.com/zyachel/quetre/commit/b8a0b9fcadd1d3c797b0023ff91b2221b9072298))
* fix flash of inaccurate color theme ([35a0d87](https://github.com/zyachel/quetre/commit/35a0d87c133ccda9242d319afa63a2c17a6df973))
## [6.0.0](https://github.com/zyachel/quetre/compare/v5.6.0...v6.0.0) (2023-03-04)

11
Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node:alpine3.17
WORKDIR /app
RUN apk update && apk add git
RUN git clone https://github.com/zyachel/quetre .
RUN npm i -g pnpm
RUN pnpm install
EXPOSE 3000
CMD ["pnpm", "start"]

View file

@ -46,32 +46,29 @@ It enables you to see answers without ads, trackers, and other such bloat.
## Instances
<!-- prettier-ignore -->
| Instance | Region | Provider | Notes |
| -------- | ------ | -------- | ----- |
| 1. Clearnet | | | |
| [quetre.iket.me](https://quetre.iket.me) | Canada | OVHCloud | Official instance |
| [quora.vern.cc](https://qr.vern.cc) | US | Hetzner | Operated by [~vern](https://vern.cc/) |
| [quetre.pussthecat.org](https://quetre.pussthecat.org) | Germany | &ndash; | Operated by [PussTheCat.org](https://pussthecat.org/) |
| [quetre.tokhmi.xyz](https://quetre.tokhmi.xyz/) | U.S. | Oracle | Operated by [Tokhmi](https://tokhmi.xyz) |
| [quetre.projectsegfau.lt](https://quetre.projectsegfau.lt) | Europe | BuyVM | Operated by [Project Segfault](https://projectsegfau.lt) |
| [quetre.esmailelbob.xyz](https://quetre.esmailelbob.xyz) | Canada | OVHCloud | Operated by [Esmail EL BoB](https://esmailelbob.xyz/) |
| [quetre.odyssey346.dev](https://quetre.odyssey346.dev) | Poland | OVHCloud | Operated by [Odyssey346](https://odyssey346.dev/) |
| [quetre.privacydev.net](https://quetre.privacydev.net) | U.S. | BuyVM | Operated by [PrivacyDev](https://privacydev.net/) |
| [ask.habedieeh.re](https://ask.habedieeh.re) | Canada | Oracle | Operated by [habedieeh.re](https://www.habedieeh.re) |
| [quetre.marcopisco.com](https://quetre.marcopisco.com) | Portugal | Vodafone Portugal (Cloudflare) | Operated by [marcopisco.com](https://www.marcopisco.com) |
| [quetre.blackdrgn.nl](https://quetre.blackdrgn.nl) | Germany | Contabo | Operated by [blackdrgn.nl](https://blackdrgn.nl) |
| [quetre.pufe.org](https://quetre.pufe.org) | New Zealand | - | Operated by pufe.org |
| [quetre.lunar.icu](https://quetre.lunar.icu) | Germany | Cloudflare | Operated by [lunar.icu](https://lunar.icu/) |
| [que.wilbvr.me](https://que.wilbvr.me) | Netherlands | Liga Hosting | Operated by [Wilbvr](https://wilbvr.me) |
| [quora.femboy.hu](https://quora.femboy.hu) | Hungary | N/A (Self-hosted) | Operated by [hnhx](https://femboy.hu) |
| 2. Onion | | | |
| [quetre.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://quetre.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | Canada | OVHCloud | Operated by [Esmail EL BoB](https://esmailelbob.xyz/) |
| [qr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://qr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | US | Hetzner | Operated by [~vern](https://vern.cc) |
| [ask.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion](http://ask.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion/) | Canada | Oracle | Operated by [habedieeh.re](https://www.habedieeh.re) |
| [quetre.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion](http://quetre.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | U.S. | BuyVM | Operated by [PrivacyDev](https://privacydev.net/) |
| [quora.cepyxplublbyw2f4axy4pyztfbxmf63lrt2c7uwv6wl4iixz53czload.onion](http://quora.cepyxplublbyw2f4axy4pyztfbxmf63lrt2c7uwv6wl4iixz53czload.onion) | Hungary | N/A (Self-hosted) | Operated by [hnhx](https://femboy.hu) |
| 3. I2P | | | |
| [qr.vern.i2p/](http://vernnflenvsqccuanaun7yydnmturi4jkyxlyzhn6ultpje66c3q.b32.i2p/) | US | Hetzner | Operated by [~vern](https://vern.cc) |
| Instance | Tor | I2P | Region | Provider | Notes |
| ----------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ------- | -------------------- | --------------------------------------------------------------------------- |
| <https://quetre.iket.me/> | No | No | CA | OVHCloud | Official instance |
| <https://qr.vern.cc/> | [Yes](http://qr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | [Yes](http://vernnflenvsqccuanaun7yydnmturi4jkyxlyzhn6ultpje66c3q.b32.i2p/) | US | Hetzner | Operated by [~vern](https://vern.cc/) |
| <https://quetre.pussthecat.org/> | No | No | DE | &ndash; | Operated by [PussTheCat.org](https://pussthecat.org/) |
| <https://quetre.tokhmi.xyz/> | No | No | US | Oracle | Operated by [Tokhmi](https://tokhmi.xyz/) |
| <https://quetre.privacydev.net/> | [Yes](http://quetre.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion/) | No | FR | Clovux | Operated by [PrivacyDev](https://privacydev.net/) |
| <https://ask.habedieeh.re/> | [Yes](http://ask.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion/) | No | CA | Oracle | Operated by [habedieeh.re](https://www.habedieeh.re/) |
| <https://quetre.blackdrgn.nl/> | No | No | DE | Contabo | Operated by [blackdrgn.nl](https://blackdrgn.nl/) |
| <https://quetre.lunar.icu/> | No | No | DE | Cloudflare | Operated by [lunar.icu](https://lunar.icu/) |
| <https://q.opnxng.com/> | No | No | SG | Vultr | Operated by [Opnxng](https://about.opnxng.com/) |
| <https://ask.sudovanilla.org/> | No | No | US | N/A (Self-hosted) | Operated by [SudoVanilla](https://sudovanilla.org/) |
| <https://quetre.drgns.space/> | No | No | US | N/A (Self-hosted) | Operated with ❤️ from [drgns.space](https://drgns.space/) |
| <https://quetre.r4fo.com/> | No | No | NL | Oracle | Operated by [r4fo](https://r4fo.com/) |
| <https://quetre.ducks.party/> | No | No | NL | Timeweb | Operated by [ducks.party](https://ducks.party/) |
| <https://quetre.nadeko.net/> | [Yes](http://quetre.nadekobxalvyqrhvp3m2atfgdmzp5vcwdmu3wo4htecwjkodancfmgid.onion) | No | CL | Oracle | Operated by [Fijxu](https://nadeko.net) |
| <https://quetre.private.coffee/> | [Yes](http://quetre.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion) | No | AT | Alwyzon | Operated by [Private.coffee](https://private.coffee) |
| <https://quetre.canine.tools/> | No | No | US | RoyaleHosting | Operated by [canine.tools](https://canine.tools/) |
| <https://qt.bloat.cat/> | No | No | DE | Datalix | Operated by [bloat.cat](https://bloat.cat/) |
| <https://quetre.gitro.xyz> | No | No | DE | Hetzner | Operated by [Gitro](https://gitro.xyz) |
| <https://quetre.jeikobu.net> | No | No | DE | Hetzner (Cloudflare) | Operated by [Shindou Jeikobu](https://jeikobu.net) |
Instances list in JSON format can be found in [instances.json](instances.json) file.
---
@ -187,7 +184,7 @@ From [their privacy policy](https://www.quora.com/about/privacy)
## To-Do
- [x] add missing routes like topics, profile, and search
- [x] add missing routes like topics and profile
- [x] use redis
- [x] serve images and other assets from Quetre
- [x] implement a better installation method
@ -259,9 +256,14 @@ Following extensions can be used to automatically redirect Quora URLs to Quetre:
A userscript that redirects popular social media platforms to their privacy respecting frontends.
- Other addons with similar functionality:
- [Dynamic Privacy Redirect](https://github.com/PrivacyDevel/DPR-addon)
- [Alter](https://github.com/w3bdev1/alter)
- [Predirect](https://github.com/libreom/predirect), A modern, manifest v3 based extension that requires minimal permissions(even for embeds).
See [Predirect's Comparision table](https://github.com/libreom/predirect/blob/main/COMPARISON.md) for more.
### Other alternative front-ends
- [digitalblossom/alternative-frontends](https://github.com/digitalblossom/alternative-frontends): contains other alternative front-ends.
@ -312,3 +314,9 @@ Send a message on [\[matrix\]](https://matrix.to/#/@ninal:matrix.org) or go old
## License
Licensed under [GNU AGPLv3](./LICENSE).
---
## Disclaimer
*Quetre does not host any content. All content is from Quora. Quora is a tradmark of Quora Inc.*

View file

@ -7,9 +7,8 @@ import catchAsyncErrors from '../utils/catchAsyncErrors.js';
import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
import getProfile from '../fetchers/getProfile.js';
import getSearch from '../fetchers/getSearch.js';
import getOrSetCache from '../utils/getOrSetCache.js';
import { answersKey, profileKey, searchKey, topicKey } from '../utils/cacheKeys.js';
import { answersKey, profileKey, topicKey } from '../utils/cacheKeys.js';
////////////////////////////////////////////////////////
// EXPORTS
@ -18,7 +17,7 @@ export const about = (req, res, next) => {
res.status(200).json({
status: 'success',
message: `make a request.
available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug', '/search?q=query', /?q=query.`,
available endpoints are: '/slug', '/unanswered/slug', '/topic/slug', '/profile/slug'`,
});
};
@ -55,21 +54,6 @@ export const profile = catchAsyncErrors(async (req, res, next) => {
res.status(200).json({ status: 'success', data });
});
export const search = catchAsyncErrors(async (req, res, next) => {
const {
urlObj,
query: { lang },
} = req;
const searchText = urlObj.searchParams.get('q')?.trim(); // no search to perform if there isn't any query
let searchData = null;
if (searchText)
searchData = await getOrSetCache(searchKey(urlObj), getSearch, urlObj.search, lang);
res.status(200).json({ status: 'success', data: searchData });
});
export const unimplemented = (req, res, next) => {
res.status(501).json({
status: 'fail',
@ -77,6 +61,14 @@ export const unimplemented = (req, res, next) => {
});
};
export const gone = (req, res, next) => {
res.status(501).json({
status: 'fail',
message: "This route doesn't exist anymore.",
});
};
export const image = catchAsyncErrors(async (req, res, next) => {
const { domain, path } = req.params;
if (!domain.endsWith('quoracdn.net')) {

View file

@ -7,9 +7,8 @@ import getAnswers from '../fetchers/getAnswers.js';
import getTopic from '../fetchers/getTopic.js';
import { acceptedLanguages, nonSlugRoutes } from '../utils/constants.js';
import getProfile from '../fetchers/getProfile.js';
import getSearch from '../fetchers/getSearch.js';
import getOrSetCache from '../utils/getOrSetCache.js';
import { answersKey, profileKey, searchKey, topicKey } from '../utils/cacheKeys.js';
import { answersKey, profileKey, topicKey } from '../utils/cacheKeys.js';
////////////////////////////////////////////////////////
// EXPORTS
@ -101,28 +100,6 @@ export const profile = catchAsyncErrors(async (req, res, next) => {
});
});
export const search = catchAsyncErrors(async (req, res, next) => {
const {
urlObj,
query: { lang },
} = req;
const searchText = urlObj.searchParams.get('q')?.trim();
let searchData = null;
if (searchText)
searchData = await getOrSetCache(searchKey(urlObj), getSearch, urlObj.search, lang);
res.status(200).render('search', {
data: searchData,
meta: {
title: searchText || 'Search',
url: urlObj,
imageUrl: `${urlObj.origin}/icon.svg`,
description: searchText ? `results for '${searchText}'` : 'search page',
},
});
});
const regex = /^https:\/\/(.{2,})\.quora\.com(\/.*)$/; // local helper constant
export const redirect = (req, res, next) => {
const url = req.originalUrl.replace('/redirect/', ''); // removing `/redirect/` part.
@ -158,3 +135,20 @@ export const unimplemented = (req, res, next) => {
},
});
};
export const gone = (req, res, next) => {
const data = {
message: "This route doesn't exist anymore.",
statusCode: 410,
};
res.status(data.statusCode).render('error', {
data,
meta: {
title: 'Gone',
url: req.urlObj,
imageUrl: `${req.urlObj.origin}/icon.svg`,
description: data.message,
},
});
};

20
docker-compose.yml Normal file
View file

@ -0,0 +1,20 @@
version: '3'
services:
quetre:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "3000:3000"
environment:
- "NODE_ENV=production"
- "PORT=3000"
- "CACHE_PERIOD=1h"
- "REDIS_URL=redis:6379" # optional
- "REDIS_TTL=3600"
redis:
image: docker.io/redis:alpine
container_name: redis
restart: unless-stopped

View file

@ -5,6 +5,7 @@
import * as cheerio from 'cheerio';
import getAxiosInstance from '../utils/getAxiosInstance.js';
import AppError from '../utils/AppError.js';
import parse from '../utils/parse.js';
////////////////////////////////////////////////////////
// FUNCTION
@ -31,20 +32,20 @@ const answersFetcher = async (resourceStr, lang) => {
if (!matches) return;
// brittle logic, but works
const matchedPart = JSON.parse(JSON.parse(matches[1])).data;
const matchedPart = JSON.parse(parse(matches[1])).data;
// only question block has this word
if (typeof matchedPart.question?.viewerHasAnswered !== 'undefined') {
rawData.question = matchedPart.question;
// primary answer block
} else if (matchedPart.question?.answers?.edges) {
} else if (matchedPart.question?.answers?.edges?.[0].node.answer?.content) {
rawData.answers.push(matchedPart.question.answers.edges[0].node.answer);
// other answer blocks
} else if (
// eslint-disable-next-line no-underscore-dangle
matchedPart.node?.__typename === 'QuestionRelevantAnswerItem2'
matchedPart.node?.__typename === 'QuestionAnswerItem2'
) {
rawData.answers.push(matchedPart.node.answer);

View file

@ -5,6 +5,7 @@
import * as cheerio from 'cheerio';
import getAxiosInstance from '../utils/getAxiosInstance.js';
import AppError from '../utils/AppError.js';
import parse from '../utils/parse.js';
////////////////////////////////////////////////////////
// FUNCTION
@ -18,10 +19,7 @@ import AppError from '../utils/AppError.js';
* await fetcher('topic/Space-Physics'); // will return 'space physics' topic object
* await fetcher('profile/Charlie-Cheever'); // will return object containing information about charlie cheever
*/
const fetcher = async (
resourceStr,
{ keyword, lang, toEncode = true }
) => {
const fetcher = async (resourceStr, { keyword, lang, toEncode = true }) => {
try {
// as url might contain unescaped chars. so, encoding it right away
const str = toEncode ? encodeURIComponent(resourceStr) : resourceStr;
@ -30,7 +28,7 @@ const fetcher = async (
const $ = cheerio.load(res.data);
const regex = new RegExp(`"{\\\\"data\\\\":\\{\\\\"${keyword}.*\\}"`); // equivalent to /"\{\\"data\\":\{\\"searchConnection.*\}"/
const regex = new RegExp(String.raw`"{\\"data\\":\{\\"${keyword}.*?\}"`);
let rawData;
$('body script').each((i, el) => {
@ -45,7 +43,7 @@ const fetcher = async (
if (!rawData) throw new AppError("couldn't retrieve data", 500);
return JSON.parse(rawData);
return parse(rawData);
} catch (err) {
const statusCode = err.response?.status;
if (statusCode === 404) throw new AppError('Not found', 404);

View file

@ -184,7 +184,6 @@ const getProfile = async (slug, lang) => {
},
spaces: {
numActiveInSpaces: rawData.numCanContributeTribes,
numFollowingSpaces: rawData.numFollowedTribes,
spaces: rawData.followingTribesConnection.edges.map(space => ({
numItems: space.node.numItemsOfUser,
url: quetrefy(space.node.url),
@ -194,7 +193,6 @@ const getProfile = async (slug, lang) => {
})),
},
topics: {
numFollowingTopics: rawData.numFollowedTopics,
topics: rawData.expertiseTopicsConnection.edges.map(topic => ({
name: topic.node.name,
url: quetrefy(topic.node.url),

View file

@ -1,168 +0,0 @@
////////////////////////////////////////////////////////
// IMPORTS
////////////////////////////////////////////////////////
import AppError from '../utils/AppError.js';
import fetcher from './fetcher.js';
import { quetrefy } from '../utils/urlModifiers.js';
////////////////////////////////////////////////////////
// HELPER FUNCTIONS
////////////////////////////////////////////////////////
const topicCleaner = topic => ({
type: 'topic',
url: quetrefy(topic.url),
name: topic.name,
numFollowers: topic.numFollowers,
image: topic.photoUrl,
isSensitive: topic.isSensitive,
});
const spaceCleaner = space => ({
type: 'space',
numUsers: space.tribeUserCount,
url: quetrefy(space.url),
name: space.nameString,
description: space.descriptionString,
image: space.iconRetinaUrl,
isSensitive: space.isSensitive,
});
const profileCleaner = profile => ({
type: 'profile',
credential: profile.bestCredential?.translatedString,
isAnon: profile.isAnon,
name: `${profile.names[0]?.givenName} ${profile.names[0]?.familyName}`,
url: quetrefy(profile.profileUrl),
image: profile.profileImageUrl,
numFollowers: profile.followerCount,
isVerified: profile.isVerified,
isBusiness: profile.businessStatus,
isPlusUser: profile.consumerBundleActive,
});
const questionCleaner = question => ({
type: 'question',
text: JSON.parse(question.title).sections,
url: quetrefy(question.url),
isDeleted: question.isDeleted,
numFollowers: question.followerCount,
creationTime: question.creationTime,
numComments: question.numDisplayComments,
isSensitive: question.isSensitive,
});
const answerCleaner = ({ question, previewAnswer: answer }) => ({
type: 'answer',
question: {
...questionCleaner(question),
},
...(answer.originalQuestionIfDifferent && {
originalQuestion: {
text: JSON.parse(answer.originalQuestionIfDifferent.question.title).sections,
url: quetrefy(answer.originalQuestionIfDifferent.question.url),
qid: answer.originalQuestionIfDifferent.question.qid,
},
}),
isViewable: !!answer.viewerHasAccess,
text: JSON.parse(answer.content).sections,
creationTime: answer.creationTime,
updatedTime: answer.updatedTime,
numComments: answer.numDisplayComments,
numUpvotes: answer.numUpvotes,
numViews: answer.numViews,
numShares: answer.numShares,
numAnswerRequests: answer.numRequesters,
isBusinessAnswer: answer.businessAnswer,
url: quetrefy(answer.url),
isSensitive: answer.isSensitive,
author: {
uid: answer.author.uid,
isAnon: answer.author.isAnon,
image: answer.author.profileImageUrl,
isVerified: answer.author.isVerified,
isPlusUser: answer.author.consumerBundleActive,
url: quetrefy(answer.author.profileUrl),
name: `${answer.author.names[0].givenName} ${answer.author.names[0].familyName}`,
credential: answer.authorCredential?.translatedString,
},
});
const postCleaner = post => ({
type: 'post',
pid: post.pid,
isViewable: post.viewerHasAccess,
url: quetrefy(post.url),
title: JSON.parse(post.title).sections,
isDeleted: post.isDeleted,
isSensitive: post.isSensitive,
text: JSON.parse(post.content).sections,
creationTime: post.creationTime,
updatedTime: post.updatedTime,
numComments: post.numDisplayComments,
numUpvotes: post.numUpvotes,
numViews: post.numViews,
numShares: post.numShares,
author: {
uid: post.author.uid,
isAnon: post.author.isAnon,
image: post.author.profileImageUrl,
isVerified: post.author.isVerified,
isPlusUser: post.author.consumerBundleActive,
url: quetrefy(post.author.profileUrl),
name: `${post.author.names[0].givenName} ${post.author.names[0].familyName}`,
credential: post.authorCredential?.translatedString,
},
...(post.tribeItem && {
space: {
isSensitive: post.tribeItem.tribe.isSensitive,
name: post.tribeItem.tribe.nameString,
url: quetrefy(post.tribeItem.tribe.url),
image: post.tribeItem.tribe.iconRetinaUrl,
description: post.tribeItem.descriptionString,
numFollowers: post.tribeItem.tribe.numFollowers,
},
}),
});
const resultsCleaner = results => {
const cleanedResults = results.map(result => {
const resultToClean = result.node;
if (resultToClean.topic) return topicCleaner(resultToClean.topic);
if (resultToClean.tribe) return spaceCleaner(resultToClean.tribe);
if (resultToClean.post) return postCleaner(resultToClean.post);
if (resultToClean.user) return profileCleaner(resultToClean.user);
if (resultToClean.previewAnswer) return answerCleaner(resultToClean);
if (resultToClean.question) return questionCleaner(resultToClean.question);
return {};
});
return cleanedResults;
};
////////////////////////////////////////////////////////
// FUNCTION
////////////////////////////////////////////////////////
const KEYWORD = 'searchConnection';
const getSearch = async (querySlug, lang) => {
const options = { keyword: KEYWORD, lang, toEncode: false };
const res = await fetcher(`search/${querySlug}`, options);
const {
data: { [KEYWORD]: rawData },
} = JSON.parse(res);
if (!rawData)
throw new AppError(
"Search couldn't be done. Recheck the URL, or resend the request if you believe the URL is correct.",
404
);
const data = {
results: resultsCleaner(rawData.edges),
};
return data;
};
////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export default getSearch;

View file

@ -30,8 +30,6 @@ const data = {
name: rawData.name,
url: quetrefy(rawData.url),
image: rawData.photoUrl,
aliases: rawData.aliases,
numFollowers: rawData.numFollowers,
// isLocked: rawData.isLocked,
isAdult: rawData.adult,
mostViewedAuthors: rawData.mostViewedAuthors.map(author => ({

83
instances.json Normal file
View file

@ -0,0 +1,83 @@
[
{
"clearnet": "https://quetre.iket.me/",
"country": "CA"
},
{
"clearnet": "https://qr.vern.cc/",
"tor": "http://qr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion",
"i2p": "http://vernnflenvsqccuanaun7yydnmturi4jkyxlyzhn6ultpje66c3q.b32.i2p",
"country": "US"
},
{
"clearnet": "https://quetre.pussthecat.org/",
"country": "DE"
},
{
"clearnet": "https://quetre.tokhmi.xyz/",
"country": "US"
},
{
"clearnet": "https://quetre.privacydev.net/",
"tor": "http://quetre.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion",
"country": "FR"
},
{
"clearnet": "https://ask.habedieeh.re/",
"tor": "http://ask.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion",
"country": "CA"
},
{
"clearnet": "https://quetre.blackdrgn.nl/",
"country": "DE"
},
{
"clearnet": "https://quetre.lunar.icu/",
"country": "DE"
},
{
"clearnet": "https://q.opnxng.com/",
"country": "SG"
},
{
"clearnet": "https://ask.sudovanilla.org/",
"country": "US"
},
{
"clearnet": "https://quetre.drgns.space/",
"country": "US"
},
{
"clearnet": "https://quetre.r4fo.com/",
"country": "NL"
},
{
"clearnet": "https://quetre.ducks.party/",
"country": "NL"
},
{
"clearnet": "https://quetre.nadeko.net/",
"tor": "http://quetre.nadekobxalvyqrhvp3m2atfgdmzp5vcwdmu3wo4htecwjkodancfmgid.onion",
"country": "CL"
},
{
"clearnet": "https://quetre.private.coffee/",
"tor": "http://quetre.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion",
"country": "AT"
},
{
"clearnet": "https://quetre.canine.tools/"
},
{
"clearnet": "https://qt.bloat.cat/",
"country": "DE"
},
{
"clearnet": "https://quetre.gitro.xyz",
"country": "DE"
},
{
"clearnet": "https://quetre.jeikobu.net",
"country": "DE"
}
]

View file

@ -1,6 +1,6 @@
{
"name": "quetre",
"version": "6.0.0",
"version": "8.0.0",
"description": "a libre front-end for Quora",
"private": true,
"type": "module",
@ -28,26 +28,26 @@
},
"homepage": "https://github.com/zyachel/quetre#readme",
"dependencies": {
"axios": "^0.27.2",
"axios": "^1.6.8",
"cheerio": "1.0.0-rc.12",
"compression": "^1.7.4",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"helmet": "^5.1.1",
"ioredis": "^5.3.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"morgan": "^1.10.0",
"pug": "^3.0.2",
"sass": "^1.57.1"
"sass": "^1.74.1"
},
"devDependencies": {
"@eslint/create-config": "^0.3.1",
"@types/express": "^4.17.15",
"eslint": "^8.31.0",
"@eslint/create-config": "^1.0.0",
"@types/express": "^4.17.21",
"eslint": "^9.0.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.26.0",
"nodemon": "^2.0.20",
"prettier": "^2.8.2"
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"nodemon": "^3.1.0",
"prettier": "^3.2.5"
},
"nodemonConfig": {
"ignore": [

1828
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,27 @@
////////////////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////////////////
const rootEl = document.documentElement;
const headerEl = document.querySelector('.header');
const [metaThemeEl, tempMetaThemeEl] = document.querySelectorAll(
'meta[name="theme-color"]'
);
const [metaThemeEl, tempMetaThemeEl] = document.querySelectorAll('meta[name="theme-color"]');
const btnTheme = document.querySelector('.theme-changer');
////////////////////////////////////////////////////////
// HELPER FUNCTIONS
////////////////////////////////////////////////////////
// gets theme prefered by browser
const browserPrefersDarkTheme = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
// gets theme prefered by user(stored in local storage)
const userPrefersTheme = localStorage?.getItem('theme');
// sets theme to local storage
const setTheme = theme => rootEl.setAttribute('theme', theme);
const localStorageAccessible = !!typeof Storage;
const setMetaTheme = () => {
const headerColor = window.getComputedStyle(headerEl).backgroundColor;
metaThemeEl.setAttribute('content', headerColor);
};
////////////////////////////////////////////////////////
// EVENT LISTENER
////////////////////////////////////////////////////////
btnTheme.addEventListener('click', () => {
const curTheme = rootEl.getAttribute('theme') || 'light';
const curTheme = document.documentElement.getAttribute('theme') ?? 'light';
const themeToSet = curTheme === 'light' ? 'dark' : 'light';
const colorToAdd = window.getComputedStyle(headerEl).backgroundColor;
setTheme(themeToSet);
// changes the meta theme-color tag to match the header color
metaThemeEl.setAttribute('content', colorToAdd);
// only setting the value in localStoage if it's actually accessible
if (localStorageAccessible) localStorage.setItem('theme', themeToSet);
if (isLocalStorageAccessible()) localStorage.setItem('theme', themeToSet);
setMetaTheme();
});
////////////////////////////////////////////////////////
@ -40,13 +29,11 @@ btnTheme.addEventListener('click', () => {
////////////////////////////////////////////////////////
(() => {
// setting this attr on root to not render some css styles
rootEl.setAttribute('js-enabled', '');
document.documentElement.setAttribute('js-enabled', '');
// removing duplicate meta theme color(it's initially for those who haven't enabled js)
metaThemeEl.removeAttribute('media');
tempMetaThemeEl.remove();
// applying theme preferences in case they exist
if (userPrefersTheme) setTheme(userPrefersTheme);
else if (browserPrefersDarkTheme) setTheme('dark');
setMetaTheme();
})();
////////////////////////////////////////////////////////

View file

@ -0,0 +1,24 @@
////////////////////////////////////////////////////////
// HELPER FUNCTIONS
////////////////////////////////////////////////////////
const isLocalStorageAccessible = () => {
try {
window.localStorage.getItem('test');
return true;
} catch {
return false;
}
};
const setTheme = theme => document.documentElement.setAttribute('theme', theme);
const userPrefersTheme = isLocalStorageAccessible() ? localStorage.getItem('theme') : null;
const browserPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
////////////////////////////////////////////////////////
// INIT FUNCTION
////////////////////////////////////////////////////////
(() => {
// applying theme preferences in case they exist
if (userPrefersTheme) setTheme(userPrefersTheme);
else if (browserPrefersDarkTheme) setTheme('dark');
})();

View file

@ -6,13 +6,13 @@ import {
topic,
image,
profile,
search,
gone,
} from '../controllers/apiController.js';
const apiRouter = express.Router();
apiRouter.get('/(|search)', search);
apiRouter.get('/about', about);
apiRouter.get('/search', gone);
apiRouter.get('/(|about)', about);
apiRouter.get('/image/:domain/:path', image);
apiRouter.get('/profile/:name', profile);
apiRouter.get('/topic/:slug', topic);

View file

@ -6,14 +6,14 @@ import {
topic,
unimplemented,
profile,
search,
gone,
redirect,
} from '../controllers/viewController.js';
const viewRouter = express.Router();
viewRouter.get('/(|search)', search); // search on / or /search
viewRouter.get('/about', about);
viewRouter.get('/search', gone);
viewRouter.get('/(|about)', about);
viewRouter.get('/privacy', privacy);
viewRouter.get('/profile/:name', profile);
viewRouter.get('/topic/:slug', topic);

View file

@ -8,11 +8,6 @@ const formatSlug = (slug, charToRemove) =>
////////////////////////////////////////////////////////
// EXPORTS
////////////////////////////////////////////////////////
export const searchKey = urlObj => {
const slug = formatSlug(urlObj.search, '?');
return `search:${slug}`;
};
export const answersKey = urlObj => {
const slug = formatSlug(urlObj.pathname, '/');
const lang = getLang(urlObj);

View file

@ -4,7 +4,10 @@ const ttl = process.env.REDIS_TTL || 3600;
const getOrSetCache = async (key, callback, ...callbackArgs) => {
const data = await redis.get(key);
if (data) return JSON.parse(data);
if (data) {
await redis.expire(key, ttl);
return JSON.parse(data);
}
const dataToCache = await callback(...callbackArgs);
await redis.set(key, JSON.stringify(dataToCache), 'EX', ttl);

11
utils/parse.js Normal file
View file

@ -0,0 +1,11 @@
const invalidLessThan = /\\x3C/g;
const validLessThan = '\\u003C';
/**
* parses and corrects invalid escape sequences
* @param {string} data
* @returns {Record<PropertyKey, any>}
*/
const parse = data => JSON.parse(data.replace(invalidLessThan, validLessThan));
export default parse;

View file

@ -6,6 +6,7 @@ const redisUrl = process.env.REDIS_URL;
const stub = {
get: async key => {},
set: async (key, value, secondsToken, seconds) => {},
expire: async (key, seconds) => {},
};
const redis = redisUrl ? new Redis(redisUrl) : stub;

View file

@ -32,6 +32,7 @@ html(lang='en')
meta(name='twitter:description', content=meta.description)
meta(name='twitter:image', content=meta.imageUrl)
//- own script
script(src='/js/restore-theme.js')
script(src='/js/index.js', defer)
//- initially setting this var to false. will update in case an answer contains some math expression

View file

@ -11,12 +11,9 @@ footer.footer(class=`${meta.title ==='About' ? 'footer__about' : ''}`)
ul.footer__nav
- if (meta.title !=='About')
li.footer__nav-item: a.footer__nav-link.footer__link(href="/about") About
li.footer__nav-item: a.footer__nav-link.footer__link(href="/search") Search
li.footer__nav-item: a.footer__nav-link.footer__link(href="https://github.com/zyachel/quetre") Source Code
li.footer__nav-item: a.footer__nav-link.footer__link(href="/privacy") Privacy
li.footer__nav-item: a.footer__nav-link.footer__link(href="#") Back to top
//- LICENSE
p.footer__license Licensed under&nbsp;
a.footer__link(href="https://www.gnu.org/licenses/agpl-3.0.html") GNU AGPLv3
| .
em.footer__license Quetre does not host any content. All content is from Quora. Quora is a tradmark of Quora Inc.

View file

@ -10,8 +10,6 @@ header.header(class=`${meta.title === 'About' ? 'header__about': ''}`)
//- BUTTON FOR CHANGING THEME
.header__misc
a.link.header__search(href="/search", aria-label='search page')
svg.icon: use(href='/misc/sprite.svg#icon-search')
button.button.theme-changer.header__theme(aria-label='Change Theme')
svg.icon.icon__theme.theme-changer__icon.theme-changer__icon--sun: use(href='/misc/sprite.svg#icon-sun')
svg.icon.icon__theme.theme-changer__icon.theme-changer__icon--moon: use(href='/misc/sprite.svg#icon-moon')

View file

@ -20,7 +20,7 @@ mixin quetrefyUrl(url, text)
if (match) {
const [_, subdomain, rest] = match;
if (acceptedLanguages.includes(subdomain)) link = link = `${rest}${rest.includes('?') ? '&' : '?'}lang=${subdomain}`;
if (acceptedLanguages.includes(subdomain)) link = `${rest}${rest.includes('?') ? '&' : '?'}lang=${subdomain}`;
else if (subdomain === 'www') link = rest;
else link = `/space/${subdomain}${rest}`;
} else {

View file

@ -145,8 +145,7 @@ block content
section.profile__spaces.profile-spaces
h2.heading.heading__secondary Spaces
p.profile-spaces__info
span.profile-spaces__item Active in #{data.spaces.numActiveInSpaces},&nbsp;
span.profile-spaces__item following #{data.spaces.numFollowingSpaces} spaces
span.profile-spaces__item Active in #{data.spaces.numActiveInSpaces} spaces
ul.profile-spaces__list
each space in data.spaces.spaces
li.metadata-primary.profile-spaces__list-item
@ -161,8 +160,6 @@ block content
section.profile__topics.profile-topics
h2.heading.heading__secondary.profile-topics__heading Topics
p.profile-topics__info
span.profile-topics__item Following #{data.topics.numFollowingTopics} topics
ul.profile-topics__list
each topic in data.topics.topics
li.metadata-primary.profile-topics__list-item

View file

@ -1,89 +0,0 @@
//-//////////////////////////////////////////////////////
//- INCLUDES/EXTENDS
//-//////////////////////////////////////////////////////
extends ../base
include ../mixins/_formatText
include ../mixins/_utils
include ../mixins/_answer
include ../mixins/_question
include ../mixins/_post
include ../mixins/_metadata
//-//////////////////////////////////////////////////////
//- MAIN CONTENT
//-//////////////////////////////////////////////////////
block content
main#main(class=`main search ${data ? '' :'search--no-results'}`)
-
const typesArr = [{key: 'question', text: 'Questions'}, {key: 'answer', text: 'Answers'}, {key: 'post', text: 'Posts'}, {key: 'profile', text: 'Profiles'}, {key: 'topic', text: 'Topics'}, {key: 'tribe', text: 'Spaces'}]
const timesArr = [{key: 'hour', text: 'Hour'}, {key: 'day', text: 'Day'}, {key: 'week', text: 'Week'}, {key: 'month', text: 'Month'}, {key: 'year', text: 'Year'}]
const languagesArr = ['en','es','fr','de','it','jp','id','pt','hi','nl','da','fi','nb','sv','mr','bn','ta','ar','he','gu','kn','ml','te','po'];
form.search__form.search-form(action="/search", method='get', autocomplete='off', name='search')
.search-form__search-container
input.search-form__searchbar(type="search", name="q", placeholder='Enter your query...', minlength='3', aria-label='search for anything')
button.search-form__button.search-form__button--reset(type="reset", aria-label='clear searchbar and filters'): svg.icon: use(href='/misc/sprite.svg#icon-cross')
button.search-form__button.search-form__button--submit(type="submit", aria-label='search'): svg.icon: use(href='/misc/sprite.svg#icon-search')
.search-form__filters-container.search-form__filters-container--type
p.search-form__filters-heading Filter by Type
each item in typesArr
.search-form__filters-group
input.search-form__radio.visually-hidden(type="radio", name="type", value=item.key, id=`type--${item.key}`)
label.search-form__label(for=`type--${item.key}`)= item.text
.search-form__filters-container.search-form__filters-container--time
p.search-form__filters-heading Filter by Time
each item in timesArr
.search-form__filters-group
input.search-form__radio.visually-hidden(type="radio", name='time', value=item.key, id=`time--${item.key}`)
label.search-form__label(for=`time--${item.key}`)= item.text
.search-form__filters-container.search-form__filters-container--lang
p.search-form__filters-heading Filter by Language
each lang in languagesArr
.search-form__filters-group
input.search-form__radio.visually-hidden(type="radio", name='lang', value=lang, id=`lang--${lang}`)
label.search-form__label(for=`lang--${lang}`)= lang.toUpperCase()
//- TODO: refactor 'profile', 'topic', and 'space' into resusable mixins.
- if (data?.results)
section.search__results.search-results
h1.heading.heading__primary Results
- if (data.results.length === 0)
p No results found for the query. Try being less specific and/or removing filters.
- else
.search-results__container
each item in data.results
.search-results__item
- if (item.type === 'answer')
+addAnswer(item, true)
- else if (item.type === 'question')
+addQuestion(item)
- else if (item.type === 'post')
+addPost(item)
- else if (item.type === 'profile')
.metadata-primary
p.metadata-primary__heading
if item.isAnon
span Anonymous
else
a.link.metadata-primary__link(href=item.url)= item.name
if item.isVerified
svg.icon.metadata-primary__icon
title verified
use(href='/misc/sprite.svg#icon-verified')
+proxifyImg(item.image)(class='metadata-primary__image', alt='', aria-hidden='true')
p.metadata-primary__misc(aria-label=`${item.name}'s credentials`)= item.credential || ''
- else if (item.type === 'topic')
.metadata-primary
a.link.metadata-primary__heading(href=item.url)= item.name
+proxifyImg(item.image)(class='metadata-primary__image', aria-hidden='true', alt='')
p.metadata-primary__misc
+formatNumber(item.numFollowers)
|&nbsp;Followers
- else if (item.type === 'space')
.metadata-primary.profile-spaces__list-item
+proxifyImg(item.image)(class='metadata-primary__image', alt='', aria-hidden='true')
a.link.metadata-primary__heading(href=item.url)= item.name
p.metadata-primary__misc= item.description

View file

@ -15,12 +15,7 @@ block content
.metadata-primary
h1.heading.heading__primary.metadata-primary__heading.topic__name= data.name
+proxifyImg(data.image)(class='metadata-primary__image', alt='', aria-hidden='true')
p.metadata-primary__misc
if data.aliases.length
span Also known as:&nbsp;
span= data.aliases.join(', ')
.metadata-secondary
+addMetadataSecondary('user','Followers', data.numFollowers)
if data.isAdult
+addMetadataSecondary('danger', 'Adult Topic', '18+', true)
+quorafyUrl(meta.url)(class='link')

View file

@ -441,6 +441,11 @@
&__question-item {
font-weight: 500;
}
&__question-link[href^="/unanswered/"]::before {
content: '[Unanswered] ';
color: var(--clr-base-text-alt-alpha);
}
}
////////////////////////////////////////////////////////
@ -531,119 +536,3 @@
gap: var(--space-800);
}
}
////////////////////////////////////////////////////////
// SEARCH PAGE COMPONENTS
////////////////////////////////////////////////////////
///
.search-form {
display: grid;
gap: var(--space-200);
// justify-items: center;
&__search-container {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(15rem, auto) 3rem 3rem;
grid-auto-rows: 3rem;
border: 1px solid var(--clr-base-icon-alt-alpha);
border-radius: 100vmax;
overflow: hidden;
padding: var(--space-100);
gap: var(--space-050);
&:focus-within {
background-color: var(--clr-code-bg);
color: var(--clr-code-text);
}
}
&__searchbar {
outline: none;
border: none;
background-color: transparent;
font: inherit;
caret-color: var(--clr-base-icon);
// fix for browsers with non-standard properties. yes, webkit and blink suck.
-webkit-appearance: none;
&::-webkit-search-cancel-button {
display: none;
}
}
&__button {
border: none;
background: transparent;
cursor: pointer;
border-radius: 100vmax;
&:hover {
transform: scale(1.1);
}
&--reset {
}
&--submit {
}
}
&__filters-container {
display: flex;
flex-wrap: wrap;
gap: 2rem 1rem;
}
&__filters-heading {
flex-basis: 100%;
}
&__filters-group {
position: relative;
}
&__radio {
}
&__label {
cursor: pointer;
background: var(--clr-code-bg);
color: var(--clr-code-text);
padding: 1rem;
border-radius: 1rem;
}
&__radio:checked + &__label {
color: var(--clr-selection-text);
background: var(--clr-selection-bg);
}
&__radio:focus + &__label {
@include focus-rules;
}
@supports selector(:focus-visible) {
&__radio:focus + &__label {
outline: none;
}
&__radio:focus-visible + &__label {
@include focus-rules;
}
}
}
.search-results {
--img-dim: var(--fs-600);
display: grid;
gap: var(--space-500);
&__container {
display: grid;
gap: var(--space-500);
}
&__item {
}
}

View file

@ -81,8 +81,7 @@
gap: var(--space-200);
}
&__theme,
&__search {
&__theme {
height: var(--fs-300);
width: var(--fs-300);
@ -92,12 +91,6 @@
}
}
// search icon viewBox is larger than content, hence the fix
&__search > svg {
height: 105%;
width: 105%;
}
&__info {
display: grid;
place-items: center;

View file

@ -405,54 +405,3 @@
padding-inline: var(--space-200);
}
}
////////////////////////////////////////////////////////
// SEARCH
////////////////////////////////////////////////////////
.search {
// justify-self: center;
padding: var(--space-500) var(--space-800);
display: grid;
grid-template-columns: 2fr 1.2fr;
gap: var(--space-500);
align-items: start;
&--no-results {
grid-template-columns: unset;
place-content: center;
max-width: 100rem;
margin-inline: auto;
}
&__results {
grid-column: 1 / 2;
grid-row: 1 / 2;
}
&__form {
grid-row: 1 / 2;
grid-column: -2 / -1;
}
@include respond-to(bp-1200) {
padding: var(--space-500);
gap: var(--space-500);
}
@include respond-to(bp-900) {
grid-template-columns: auto;
grid-template-rows: max-content auto;
&__results {
grid-row: 2 / 3;
}
&__form {
grid-row: 1 / 2;
}
}
@include respond-to(bp-550) {
padding-inline: var(--space-200);
}
}