├── .idx └── dev.nix ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── helperfunctions.js ├── images ├── AboutMe.png ├── Heart.png ├── Highlight.png ├── Logo.png ├── MFP.png ├── MLP.png ├── MVP.png ├── Star.png ├── View.png ├── WIWO.png ├── search.png └── searchbar.png ├── index.html ├── scripts.js └── style.css /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | # To learn more about how to use Nix to configure your environment 2 | # see: https://developers.google.com/idx/guides/customize-idx-env 3 | { pkgs, ... }: { 4 | # Which nixpkgs channel to use. 5 | channel = "stable-24.05"; # or "unstable" 6 | 7 | # Use https://search.nixos.org/packages to find packages 8 | packages = [ 9 | # pkgs.go 10 | # pkgs.python311 11 | # pkgs.python311Packages.pip 12 | # pkgs.nodejs_20 13 | # pkgs.nodePackages.nodemon 14 | ]; 15 | 16 | # Sets environment variables in the workspace 17 | env = {}; 18 | idx = { 19 | # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" 20 | extensions = [ 21 | # "vscodevim.vim" 22 | ]; 23 | 24 | # Enable previews 25 | previews = { 26 | enable = true; 27 | previews = { 28 | web = { 29 | # Example: serve the current directory with the command `npx serve .`. 30 | # and show it in IDX's web preview panel 31 | command = ["npx" "serve" "."]; 32 | manager = "web"; 33 | env = { 34 | # Environment variables to set for your server 35 | PORT = "$PORT"; 36 | }; 37 | }; 38 | 39 | }; 40 | }; 41 | 42 | # Workspace lifecycle hooks 43 | workspace = { 44 | # Runs when a workspace is first created 45 | onCreate = { 46 | # Example: install JS dependencies from NPM 47 | # npm-install = "npm install"; 48 | }; 49 | # Runs when the workspace is (re)started 50 | onStart = { 51 | # Example: start a background task to watch and re-build backend code 52 | # watch-backend = "npm run watch-backend"; 53 | }; 54 | }; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDX.aI.enableInlineCompletion": true, 3 | "IDX.aI.enableCodebaseIndexing": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scratchstats 2 | -------------------------------------------------------------------------------- /helperfunctions.js: -------------------------------------------------------------------------------- 1 | export function hyperLinkText(text, href) { 2 | const link = document.createElement('a'); 3 | link.href = `?user=${href}`; 4 | link.textContent = text; 5 | return link; 6 | } 7 | 8 | export function findandReplaceMentions(text) { 9 | const mentionRegex = /@([a-zA-Z0-9_-]+)/g; 10 | return text.replace(mentionRegex, (_match, username) => { 11 | const link = hyperLinkText(`@${username}`, username); 12 | return link.outerHTML; 13 | }) 14 | } 15 | 16 | export async function fetchUrl(url) { 17 | const res = await fetch('https://corsproxy.io/?url=' + encodeURIComponent(url)); 18 | if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); 19 | return await res.json(); 20 | } 21 | 22 | export function sanitizeText(text) { 23 | const temp = document.createElement('div'); 24 | temp.innerText = text; 25 | return temp.innerHTML; 26 | } -------------------------------------------------------------------------------- /images/AboutMe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/AboutMe.png -------------------------------------------------------------------------------- /images/Heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/Heart.png -------------------------------------------------------------------------------- /images/Highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/Highlight.png -------------------------------------------------------------------------------- /images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/Logo.png -------------------------------------------------------------------------------- /images/MFP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/MFP.png -------------------------------------------------------------------------------- /images/MLP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/MLP.png -------------------------------------------------------------------------------- /images/MVP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/MVP.png -------------------------------------------------------------------------------- /images/Star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/Star.png -------------------------------------------------------------------------------- /images/View.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/View.png -------------------------------------------------------------------------------- /images/WIWO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/WIWO.png -------------------------------------------------------------------------------- /images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/search.png -------------------------------------------------------------------------------- /images/searchbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoupleCodes/scratchstats/b8039f7b991f49187969855f9d1b3d9b20348d15/images/searchbar.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scratchstats 5 | 6 | 7 | 8 | 9 |
10 | 21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 43 | 44 | -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | import { fetchUrl, findandReplaceMentions, sanitizeText } from './helperfunctions.js' 2 | 3 | export async function getUser(fetchUser) { 4 | const url = "https://api.scratch.mit.edu/users/" + fetchUser 5 | try { 6 | const data = await fetchUrl(url); 7 | var userside = document.getElementById("userside") 8 | var userInfoHTML = document.getElementById("userinfo") 9 | var href = new URL(window.location.href); 10 | href.searchParams.set('user', fetchUser); 11 | history.replaceState(null, null, href.toString()) 12 | 13 | let projects = []; 14 | let projectData = []; 15 | let offset = 0; 16 | 17 | if (data) { 18 | userInfoHTML.innerHTML = ` 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | `; 27 | 28 | try { 29 | while (true) { 30 | let projectUrl = url + `/projects?offset=${offset}&limit=40`; 31 | projectData = await fetchUrl(projectUrl); 32 | if (projectData.length > 0) { 33 | projects.push(...projectData); 34 | offset += 40; 35 | if (projectData.length < 40) { break } 36 | } else { break } 37 | } 38 | 39 | const sortBy = prop => projects => projects.slice().sort((a, b) => b.stats[prop] - a.stats[prop]); 40 | const getTop = projects => projects.slice(0, 25); 41 | const sum = prop => projects => projects.reduce((acc, p) => acc + p.stats[prop], 0); 42 | 43 | const totalViews = sum('views')(projects); 44 | const totalLoves = sum('loves')(projects); 45 | const totalFavorites = sum('favorites')(projects); 46 | 47 | const avg = prop => Math.round(prop / projects.length) || 0; 48 | const [averageViews, averageLoves, averageFavorites] = [avg(totalViews), avg(totalLoves), avg(totalFavorites)]; 49 | 50 | userside.innerHTML = 51 | ` 52 |
53 |

54 |

${data.username || 'Unknown'}

55 |

Location:

56 |

${data.profile.country || 'Unknown'}

57 |
58 |

Joined:

59 |

${new Date(data.history.joined).toLocaleString() || 'Unknown'}

60 |
61 |
62 |
Project Stats:
63 | 68 |
69 |
70 |
Average Project Stats:
71 | 76 |
77 | ` 78 | const [topViewed, topLoved, topFavorited] = ['views', 'loves', 'favorites'].map(prop => getTop(sortBy(prop)(projects))); 79 | 80 | function displayTopProjects(projects, elementId, statistic) { 81 | const list = document.getElementById(elementId); 82 | const projectsDIV = document.createElement('div'); 83 | 84 | if (list) { 85 | Object.assign(projectsDIV.style, { display: 'flex', flexDirection: 'row', gap: '5px', maxWidth: '458px', overflowY: 'auto' }); 86 | 87 | projects.forEach(project => { 88 | const projectDiv = document.createElement('div'); 89 | Object.assign(projectDiv.style, { display: 'flex', flexDirection: 'column' }); 90 | projectDiv.classList.add('project'); 91 | projectDiv.innerHTML = 92 | ` 93 | 94 | 95 |

${project.title}

96 |
97 |
98 | 106 | ${project.stats[statistic]} 107 |
108 | `; 109 | projectsDIV.appendChild(projectDiv); 110 | }); 111 | } 112 | if (list) { list.appendChild(projectsDIV); } 113 | } 114 | 115 | displayTopProjects(topViewed, "most-viewed", "views"); 116 | displayTopProjects(topLoved, "most-loved", "loves"); 117 | displayTopProjects(topFavorited, "most-favorited", "favorites"); 118 | 119 | const aboutMeDiv = document.querySelector("#aboutme > div") 120 | const wiwoDiv = document.querySelector("#wiwo > div"); 121 | const preBio = document.createElement('pre'); 122 | const preWiwo = document.createElement('pre'); 123 | 124 | preBio.innerHTML = findandReplaceMentions(sanitizeText(data.profile.bio)); 125 | preWiwo.innerHTML = findandReplaceMentions(sanitizeText(data.profile.status)); 126 | 127 | aboutMeDiv.appendChild(preBio); 128 | wiwoDiv.appendChild(preWiwo); 129 | 130 | } catch (error) { 131 | console.error("Error fetching projects:", error); 132 | } 133 | document.title = "Scratchstats - " + data.username 134 | 135 | } else { 136 | userside.innerHTML = `

User not found

` 137 | userInfoHTML.innerHTML = "" 138 | } 139 | } catch (error) { 140 | userInfoHTML.innerHTML = "ERROR" 141 | } 142 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | #container { 2 | margin: 50px auto; 3 | max-width: 780px; 4 | width: 100%; 5 | font-family: Tahoma, sans-serif; 6 | } 7 | 8 | .clearme:after { 9 | clear: both; 10 | content: "."; 11 | display: block; 12 | height: 0; 13 | visibility: hidden; 14 | } 15 | 16 | .sidecontainer { 17 | background-image: url('./images/Highlight.png'); 18 | background-repeat: no-repeat; 19 | min-height: 80px; 20 | overflow: auto; 21 | padding: 15px 50px 10px 10px; 22 | } 23 | 24 | #logo { 25 | float: left; 26 | } 27 | 28 | #aboutme div, #wiwo div { 29 | max-height: 150px; 30 | max-width: 458px; 31 | overflow: auto; 32 | } 33 | 34 | #user-img { 35 | float: right; 36 | width: 56px; 37 | height: 56px; 38 | border: 2px solid grey; 39 | } 40 | 41 | #username { 42 | padding-bottom: 9px; 43 | font-size: 14px; 44 | } 45 | 46 | #stats ul { 47 | padding: 0; 48 | margin: 6px; 49 | } 50 | 51 | p {margin:0} 52 | 53 | li { 54 | display: block; 55 | img { 56 | vertical-align: middle; 57 | } 58 | } 59 | 60 | #searchbar { 61 | width: 322px; 62 | height: 35px; 63 | background-image: url('./images/searchbar.png') !important; 64 | background-size: cover; 65 | border: none; 66 | } 67 | 68 | #userside { 69 | float: left; 70 | margin-right: 10px; 71 | margin-top: 5px; 72 | width: 230px; 73 | } 74 | 75 | #userinfo { 76 | display: table-row; 77 | } 78 | 79 | input { 80 | padding-left: 16px; 81 | 82 | } 83 | 84 | input:focus { 85 | outline: none; 86 | background-color: white; 87 | } 88 | 89 | .search-container { 90 | display: block; 91 | float: right; 92 | } 93 | 94 | #search-field { 95 | display: flex; 96 | img { 97 | height: 35px; 98 | display: block; 99 | } 100 | } 101 | 102 | .project { 103 | font-size: 11px; 104 | width: 85px; 105 | p { 106 | white-space: nowrap; 107 | overflow: hidden; 108 | text-overflow: ellipsis; 109 | } 110 | } 111 | 112 | pre { 113 | font-size: 12px; 114 | font-family: Tahoma; 115 | margin: 0; 116 | white-space: pre-wrap; 117 | } --------------------------------------------------------------------------------