3 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/icon white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/icon white.png
--------------------------------------------------------------------------------
/assets/banner-blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/assets/banner-blank.png
--------------------------------------------------------------------------------
/static/emojis/ocular.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/emojis/ocular.png
--------------------------------------------------------------------------------
/static/emojis/squirrel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/emojis/squirrel.png
--------------------------------------------------------------------------------
/plugins/vue-tooltip.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VTooltip from 'v-tooltip'
3 |
4 | Vue.use(VTooltip)
--------------------------------------------------------------------------------
/static/reaction-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/reaction-screenshot.png
--------------------------------------------------------------------------------
/components/Emoji.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/plugins/vue-good-table.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueGoodTablePlugin from 'vue-good-table';
3 |
4 | // import the styles
5 | import 'vue-good-table/dist/vue-good-table.css'
6 |
7 | Vue.use(VueGoodTablePlugin);
--------------------------------------------------------------------------------
/assets/README.md:
--------------------------------------------------------------------------------
1 | # ASSETS
2 |
3 | **This directory is not required, you can delete it if you don't want to use it.**
4 |
5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
6 |
7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
8 |
--------------------------------------------------------------------------------
/plugins/README.md:
--------------------------------------------------------------------------------
1 | # PLUGINS
2 |
3 | **This directory is not required, you can delete it if you don't want to use it.**
4 |
5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application.
6 |
7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
8 |
--------------------------------------------------------------------------------
/middleware/README.md:
--------------------------------------------------------------------------------
1 | # MIDDLEWARE
2 |
3 | **This directory is not required, you can delete it if you don't want to use it.**
4 |
5 | This directory contains your application middleware.
6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
7 |
8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
9 |
--------------------------------------------------------------------------------
/plugins/auth.js:
--------------------------------------------------------------------------------
1 | export default ({store}, inject) => {
2 | // Inject $hello(msg) in Vue, context and store.
3 | let auth = {
4 | loggedIn() {
5 | return !!store.state.auth.user
6 | },
7 | user() {
8 | return store.state.auth.user
9 | },
10 | token() {
11 | return store.state.auth.token
12 | }
13 | }
14 | inject('auth', auth)
15 | }
--------------------------------------------------------------------------------
/static/original icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/browse/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
browse topics
6 |
note: this feature is in beta and may be changed at any time. it's also a bit slow due to scratchdb things.
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/static/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/store/README.md:
--------------------------------------------------------------------------------
1 | # STORE
2 |
3 | **This directory is not required, you can delete it if you don't want to use it.**
4 |
5 | This directory contains your Vuex Store files.
6 | Vuex Store option is implemented in the Nuxt.js framework.
7 |
8 | Creating a file in this directory automatically activates the option in the framework.
9 |
10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
11 |
--------------------------------------------------------------------------------
/components/PostCount.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ count || '0+' }} posts
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/components/PostTime.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ new Date(time) }}
4 | {{ new Date(time).toLocaleDateString("en-US") + ' - ' + new Date(time).toLocaleTimeString("en-US") }}
5 |
6 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/content/docs/gallery.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: ocular api usage gallery
3 | headertitle: api gallery
4 | description: how are people using the ocular api?
5 | ---
6 |
7 | - a browser extension. i wish i could tell you which one, but [no](/topic/284272).
8 | - [Scratch Forum Leaderboards](https://shefwerld.rirurin.com/post/)
9 | - [postpercent](https://postpercent.rirurin.com/)
10 | - [Scratory](https://scratory.vercel.app/)
11 | - [Scratch Tools](https://scratchtools.edu.eu.org/)
12 | - [Magnifier](https://magnifier.potatophant.net/)
13 |
14 | want to add something? [edit this page](https://github.com/jeffalo/ocular/blob/main/content/docs/gallery.md)
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ocular",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nuxt",
7 | "build": "nuxt build",
8 | "start": "nuxt start",
9 | "generate": "nuxt generate"
10 | },
11 | "dependencies": {
12 | "@nuxt/content": "^1.15.1",
13 | "@nuxtjs/redirect-module": "^0.3.1",
14 | "cookie": "^0.4.2",
15 | "core-js": "^3.37.1",
16 | "js-cookie": "^2.2.1",
17 | "nuxt": "^2.17.4",
18 | "v-tooltip": "^2.1.3",
19 | "vue-good-table": "^2.21.11",
20 | "vue-plausible": "^1.3.2"
21 | },
22 | "devDependencies": {
23 | "@nuxtjs/color-mode": "^2.1.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/components/PostList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/------feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F468\U0001F4BB feature request"
3 | about: do you have a new feature for ocular or my ocular? if so this is the template
4 | for you.
5 | title: ''
6 | labels: ''
7 | assignees: ''
8 |
9 | ---
10 |
11 | **Is your feature request related to a problem? Please describe.**
12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
13 |
14 | **Describe the solution you'd like**
15 | A clear and concise description of what you want to happen.
16 |
17 | **Describe alternatives you've considered**
18 | A clear and concise description of any alternative solutions or features you've considered.
19 |
20 | **Additional context**
21 | Add any other context or screenshots about the feature request here.
22 |
--------------------------------------------------------------------------------
/middleware/admin.js:
--------------------------------------------------------------------------------
1 | const serverCookie = process.server ? require('cookie') : undefined
2 | const clientCookie = process.client ? require('js-cookie') : undefined
3 |
4 | // make sure to keep this the same as authetnicated.js
5 |
6 | export default async function ({ $auth, redirect, req, store }) {
7 | let token = null
8 | if (process.server) {
9 | if (req.headers.cookie) {
10 | const parsed = serverCookie.parse(req.headers.cookie)
11 | token = parsed['my-ocular-token']
12 | }
13 | } else {
14 | token = clientCookie.get('my-ocular-token')
15 | }
16 |
17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something
18 |
19 | if (!$auth.user().admin) {
20 | return redirect('/')
21 | }
22 | }
--------------------------------------------------------------------------------
/pages/docs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
documentation home
8 |
9 |
10 | {{ doc.title }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/middleware/notauthenticated.js:
--------------------------------------------------------------------------------
1 | const serverCookie = process.server ? require('cookie') : undefined
2 | const clientCookie = process.client ? require('js-cookie') : undefined
3 |
4 | // make sure to keep this the same as authetnicated.js
5 |
6 | export default async function ({ $auth, redirect, req, store }) {
7 | let token = null
8 | if (process.server) {
9 | if (req.headers.cookie) {
10 | const parsed = serverCookie.parse(req.headers.cookie)
11 | token = parsed['my-ocular-token']
12 | }
13 | } else {
14 | token = clientCookie.get('my-ocular-token')
15 | }
16 |
17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something
18 |
19 | if ($auth.loggedIn()) {
20 | return redirect('/')
21 | }
22 | }
--------------------------------------------------------------------------------
/middleware/authenticated.js:
--------------------------------------------------------------------------------
1 | const serverCookie = process.server ? require('cookie') : undefined
2 | const clientCookie = process.client ? require('js-cookie') : undefined
3 |
4 | // make sure to keep this the same as notauthetnicated.js
5 |
6 | export default async function ({ $auth, redirect, req, store }) {
7 | let token = null
8 | if (process.server) {
9 | if (req.headers.cookie) {
10 | const parsed = serverCookie.parse(req.headers.cookie)
11 | token = parsed['my-ocular-token']
12 | }
13 | } else {
14 | token = clientCookie.get('my-ocular-token')
15 | }
16 |
17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something
18 |
19 | if (!$auth.loggedIn()) {
20 | return redirect('/login')
21 | }
22 | }
--------------------------------------------------------------------------------
/content/docs/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: about
3 | description: about ocular
4 | ---
5 |
6 | ## what is ocular?
7 | ocular is a tool for searching the [scratch forums](https://scratch.mit.edu/discuss/). it contains some cool features like statuses, reactions and starring to make using the scratch forums more fun!
8 |
9 | ## what is/was my-ocular?
10 | my-ocular previously was a site where you could set your ocular status. for technical reasons this had to be maintained separately from ocular, however in april 2021, all of my-ocular's features were migrated to ocular. my-ocular only remains as a backend/api.
11 |
12 | ## what is an ocular status?
13 | an ocular status is a short message and favourite colour, which is displayed across ocular and [other sites](/docs/gallery). an ocular status is not required to use ocular, but they allow you to customize your profile a little bit. :)
14 |
15 | [set your ocular status](/dashboard)
16 |
--------------------------------------------------------------------------------
/components/Status.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ status }}
4 |
9 |
10 |
11 |
12 |
31 |
32 |
--------------------------------------------------------------------------------
/components/TopicTime.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ date }}
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/pages/youtube/_id.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
youtube
6 |
scratch converts youtube links to links to their own player page to prevent people from seeing youtube or something. this page is here to simulate the true scratch experience.
19 |
20 |
21 |
26 |
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔍 ocular
2 | ocular is the search tool for the scratch forums.
3 |
4 | it's up at [ocular.jeffalo.net](https://ocular.jeffalo.net)
5 |
6 | ## behind the scenes
7 | - data from [DatOneLefty's ScratchDB](https://scratchdb.lefty.one/)
8 | - styling taken from [Maximouse](https://scratch.mit.edu/users/Maximouse)'s userstyle
9 | - big inspiration from [forums.scratchstats.com](https://forums.scratchstats.com)
10 | - nuxt because SSR
11 | - hosted at home
12 | - and obviously scratch!
13 |
14 | ## how to run
15 |
16 | you will need your own my-ocular server if you don't want to use production data, otherwise you can use the production my-ocular server by setting the `BACKEND_URL` env variable to `https://my-ocular.jeffalo.net`. keep in mind you will get cors errors unless you run the nuxt server on `localhost` port `8000` or `8001`. you can change the nuxt server port via the `PORT` env variable.
17 |
18 | ```bash
19 | # install dependencies
20 | $ npm install
21 |
22 | # serve with hot reload at localhost:3000
23 | $ npm run dev
24 |
25 | # build for production and launch server
26 | $ npm run build
27 | $ npm run start
28 |
29 | # generate static project
30 | $ npm run generate
31 | ```
32 |
--------------------------------------------------------------------------------
/pages/starred.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 | logging into ocular will allow you to customize your profile, save posts and more! if you do not have an account, this will create one for you
11 |
16 |
17 |
18 |
68 |
69 |
--------------------------------------------------------------------------------
/content/docs/privacy.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: privacy
3 | description: ocular privacy stuff.
4 | ---
5 |
6 | *TLDR: ocular only collects basic analytics data, registered user data and data that powers features such as reactions and starring. I don't want your private info.*
7 |
8 | ocular uses a number of services to provide various tools to enhance the experience. These services may collect and or use data according to their privacy policies.
9 |
10 | - To collect basic usage analytics, ocular uses a self-hosted instance of [Plausible Analytics](https://plausible.io). You can find out more about what kind of information is collected at the [Plausible Analytics website](https://plausible.io).
11 |
12 | - ocular uses [CloudFlare](https://cloudflare.com) to proxy requests, this keeps the site running smoothly and allows me to host ocular right from my home.
13 |
14 | - Data about Scratch forum posts comes from [ScratchDB](https://scratchdb.lefty.one), and Scratch based authentication is handled by [Scratch Auth](https://auth.itinerary.eu.org/). All other Scratch data comes straight from [the official Scratch API](https://github.com/llk/scratch-rest-api).
15 |
16 | - ocular does not store any Scratch data. ocular's purpose is to present data from the APIs listed above. Outside of analytics, ocular only stores registered user data, and data to power reactions and starring. User accounts are not required to use the base functionality of ocular.
17 |
18 | - ocular (and my other projects) are hosted on a server running NGINX. For security purposes, NGINX logs may include IP addresses.
19 |
20 | - If you would like data deletion from ocular, contact me at `jeffalobob at gmail`
21 |
22 | - For external services, please consult their sites for information about how data is stored and used, if you have any questions about your privacy when using ocular feel free to contact me about it.
23 |
--------------------------------------------------------------------------------
/store/statuses.js:
--------------------------------------------------------------------------------
1 | export const state = () => ({
2 | users: []
3 | })
4 |
5 | let findUser = (state, name) => {
6 | return state.users.find(user => user.name.toLowerCase() == name.toLowerCase())
7 | }
8 |
9 | export const actions = {
10 | async loadUser({ state, commit }, { name }) {
11 | let found = findUser(state, name)
12 | if (found && !found.loading) {
13 | // its ready, just send it
14 | return found.data
15 | }
16 |
17 | if (found && found.loading) {
18 | // its loading, return the promise
19 | return found.promise
20 | }
21 |
22 | // this is the first time
23 |
24 | let promise = new Promise((resolve, reject) => {
25 | fetch(`${process.env.backendURL}/api/user/` + name)
26 | .then((res) => res.json())
27 | .then(data => {
28 | resolve(data)
29 | commit('setUser', { name, data, promise, loading: false })
30 | })
31 | });
32 |
33 |
34 | commit('initUser', { name, promise, loading: true })
35 |
36 | return promise
37 | }
38 | }
39 |
40 | export const mutations = {
41 | setUser(state, { name, data, promise, loading }) {
42 | let found = findUser(state, name)
43 | if (found) {
44 | found = { name, data, promise, loading }
45 | } else {
46 | state.users.push({ name, data, promise, loading })
47 | }
48 | },
49 | removeUser(state, { name }) {
50 | state.users = state.users.filter(user => {
51 | return user.name !== name;
52 | })
53 | },
54 | initUser(state, { name, promise } ) {
55 | let found = findUser(state, name)
56 | if (!found) {
57 | state.users.push({ name, data: {}, promise, loading: true })
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/pages/browse/_id.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
browse topics
6 |
7 | note: this feature is in beta and may change at any time. also it's a
8 | bit slow due to scratchdb things.
9 |
33 |
34 |
35 |
103 |
104 |
136 |
137 |
--------------------------------------------------------------------------------
/content/docs/api.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API documentation
3 | headertitle: api
4 | description: ocular api documentation.
5 | ---
6 |
7 | ocular's api is integrated into a [handful of scratch-related projects](/docs/gallery). that's pretty cool in my opinion.
8 |
9 | the public ocular api is designed for retrieving data, not for setting it. in the future i might include some api token stuff, but that gets complicated and un-fun. please don't use the internal api endpoints (the ones not documented here) to update data automatically. it makes moderation harder.
10 |
11 | all api endpoints should be assumed to be under `my-ocular.jeffalo.net` unless otherwise noted.
12 |
13 | ## get an ocular user
14 |
15 | make a GET request to `/api/user/:username`
16 |
17 | you can use the query parameter `?noReplace=true` to retrieve raw status data (without replacing the elements described later)
18 |
19 | ### example response
20 |
21 | (from `https://my-ocular.jeffalo.net/api/user/Jeffalo`)
22 |
23 | ```json
24 | {
25 | "_id": "5fb91a89532f943b9046e3ba",
26 | "name": "Jeffalo",
27 | "status": "{joke}",
28 | "color": "#0fbd8c",
29 | "admin": true,
30 | "meta": {
31 | "updated": "2021-05-09T13:24:30.021Z",
32 | "updatedBy": "Jeffalo"
33 | }
34 | }
35 | ```
36 |
37 |
38 | the content of `status` is not sanitized against HTML. you are expected to do that yourself. in vanilla javascript, it is as simple as setting `innerText`, instead of `innerHTML`.
39 |
40 |
41 | note that some users have less data stored for them. you should only ever assume that `name`, `status` and `color` exist.
42 |
43 | ### example of how to display a status
44 |
45 |
46 |
47 |
48 | ## get reactions for a post
49 |
50 | make a GET request to `/api/reactions/:post-id`
51 |
52 | ### example response
53 |
54 | (from `https://my-ocular.jeffalo.net/api/reactions/5213429`)
55 |
56 | ```json
57 | [
58 | {
59 | "emoji": "👍",
60 | "reactions": [
61 |
62 | ]
63 | },
64 | {
65 | "emoji": "👎",
66 | "reactions": [
67 |
68 | ]
69 | },
70 | {
71 | "emoji": "😄",
72 | "reactions": [
73 |
74 | ]
75 | },
76 | {
77 | "emoji": "🎉",
78 | "reactions": [
79 |
80 | ]
81 | },
82 | {
83 | "emoji": "😕",
84 | "reactions": [
85 |
86 | ]
87 | },
88 | {
89 | "emoji": "❤️",
90 | "reactions": [
91 |
92 | ]
93 | },
94 | {
95 | "emoji": "🚀",
96 | "reactions": [
97 | {
98 | "_id": "60981f3ba1422d902532f0da",
99 | "post": "5213429",
100 | "user": "Jeffalo",
101 | "emoji": "🚀"
102 | }
103 | ]
104 | },
105 | {
106 | "emoji": "👀",
107 | "reactions": [
108 |
109 | ]
110 | }
111 | ]
112 | ```
113 |
114 | ## prompt to set reactions for a post
115 |
116 | this is a special page on `ocular.jeffalo.net`, it works well with the my-ocular endpoint to get reactions for a post
117 |
118 | ### use it in your site
119 |
120 | 1. open a popup for `https://ocular.jeffalo.net/react/:post-id?emoji=:emoji`
121 | 2. if you're using the my-ocular endpoint to get reactions, when the pop up is closed re-fetch the reactions.
122 |
123 | ### example screenshot
124 |
125 | (from from `https://ocular.jeffalo.net/react/5213429?emoji=🚀`)
126 |
127 | 
128 |
129 | ## ocular status secrets
130 |
131 | hey you! did you know you can include automatically updating elements to your ocular statuses? place one of these in your status make yourself look cool and knowledgeable!
132 |
133 | - `{joke}` for a funny joke
134 | - `{total}` for the total amount of registered ocular users
135 | - `{count}` for your post count
136 |
137 | ### bonus tip
138 |
139 | you can escape these with a backslash `\` if you don't want them to be replaced
140 |
--------------------------------------------------------------------------------
/components/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
140 | this is a simplified version of ocular search to replace the old
141 | broken one that depended on scratchdb v3. it is missing features (and
142 | has an incomplete database), but at least it works.
143 |