├── CNAME
├── images
├── icon.png
├── logo.png
└── favicon.ico
├── README.md
├── index.html
├── vuecomponents.js
├── 404-stats.css
├── vuecomponents.css
├── 404.html
└── 404-stats.js
/CNAME:
--------------------------------------------------------------------------------
1 | scratchstats.com
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WorldLanguages/ScratchStatsv2/HEAD/images/icon.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WorldLanguages/ScratchStatsv2/HEAD/images/logo.png
--------------------------------------------------------------------------------
/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WorldLanguages/ScratchStatsv2/HEAD/images/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScratchStats.com
2 | 100% frontend website that uses the Scratch API to show many statistics about Scratch users. Uses VueJS.
3 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ScratchStats.com
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Loading...
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/vuecomponents.js:
--------------------------------------------------------------------------------
1 | const componentsHTML = {
2 | "row": `
3 |
4 |
{{title}}
5 |
6 |
7 |
8 |
➕ Show more
9 |
10 | `,
11 | "stat": `
12 |
13 |
{{value}}
14 |
15 |
16 |
17 |
18 | `,
19 | "project": `
20 |
21 |
{{ project.title.length > 20 ? project.title.substring(0,17)+"..." : project.title }}
22 |
23 |
![]()
24 |
25 |
{{statEmojis[stat]}} {{n(project.stats[stat])}}%
26 |
27 |
28 |
29 |
30 | `
31 | };
32 |
33 | Vue.component("row", {
34 | methods: {
35 | loadMore: function(stat){
36 | this.$root.$data.projectRowShow[stat] += 3;
37 | }
38 | },
39 | props: ["title", "projectRowStat", "showLoadMore"],
40 | template: componentsHTML.row
41 | });
42 | Vue.component("stat", {
43 | props: ["value", "emoji"],
44 | template: componentsHTML.stat
45 | });
46 | Vue.component("project", {
47 | data: function() {
48 | return {
49 | statNames: this.$root.$data.statNames,
50 | statEmojis: this.$root.$data.statEmojis
51 | }
52 | },
53 | methods: {
54 | n: x => {
55 | return Math.round(x).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
56 | },
57 | openProject : id => window.open(`https://scratch.mit.edu/projects/${id}`),
58 | },
59 | props: ["project", "sortedBy", "showStats"],
60 | template: componentsHTML.project
61 | });
62 |
--------------------------------------------------------------------------------
/404-stats.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Montserrat:500,600,700,800,900,1000|Questrial");
2 |
3 | html {
4 | font-size: 1vw;
5 | }
6 |
7 | body {
8 | font-size: 1rem;
9 | color: white;
10 | background-color: #F1F1F1;
11 | font-family: Montserrat;
12 | font-weight: 600;
13 | margin: 0;
14 | }
15 |
16 | img {
17 | border-radius: .5rem;
18 | }
19 |
20 | #app {
21 | width: 100%;
22 | margin-top: 1rem;
23 | }
24 |
25 | #country-flag {
26 | width: 3rem;
27 | }
28 |
29 | #user {
30 | width: 50vw;
31 | margin: 0 auto;
32 | padding: 1rem;
33 | background-color: #3D6BFF;
34 | border-radius: 1rem;
35 | box-shadow: 0 4px 6px rgba(34,25,25,0.3);
36 | margin-bottom: 3rem;
37 | text-align: center;
38 | }
39 |
40 | #profile {
41 | cursor: pointer;
42 | }
43 |
44 | #profile-pic {
45 | width: 5rem;
46 | }
47 |
48 | #username {
49 | font-size: 2rem;
50 | overflow-x: hidden;
51 | }
52 |
53 | #buttons {
54 | display: flex;
55 | flex-direction: row;
56 | justify-content: center;
57 | flex-wrap: wrap;
58 | }
59 |
60 | .button {
61 | display: inline;
62 | border-radius: 1rem;
63 | background-color: #25aff4;
64 | font-size: 1rem;
65 | padding: .5rem;
66 | cursor: pointer;
67 | margin: .5rem;
68 | }
69 |
70 | #project-stats {
71 | margin-top: 3rem;
72 | margin-bottom: 3rem;
73 | }
74 |
75 | .explore-user {
76 | display: flex;
77 | flex-direction: column;
78 | margin-left: 3rem;
79 | margin-right: 3rem;
80 | }
81 |
82 | .explore-user-img {
83 | align-self: center;
84 | width: 5rem;
85 | height: 5rem;
86 | }
87 |
88 | #go-to-top {
89 | display: block;
90 | position: fixed;
91 | bottom: .5rem;
92 | right: .5rem;
93 | cursor: pointer;
94 | font-size: 2rem;
95 | }
96 |
97 | /*Mobile portrait*/
98 | @media screen and (max-width: 850px) and (max-aspect-ratio: 13/9) {
99 | html {
100 | font-size: 4vw;
101 | }
102 | #user {
103 | width: 80vw;
104 | }
105 | }
106 |
107 | /*Mobile landscape*/
108 | @media screen and (max-width: 850px) and (min-aspect-ratio: 13/9) {
109 | html {
110 | font-size: 1.5vw;
111 | }
112 | }
113 |
114 | /*Desktop*/
115 | @media screen and (min-width: 850px) {
116 | }
117 |
--------------------------------------------------------------------------------
/vuecomponents.css:
--------------------------------------------------------------------------------
1 | /* Rows */
2 |
3 | .row {
4 | width: 90vw;
5 | margin: 0 auto;
6 | padding-top: 1rem;
7 | background-color: #3D6BFF;
8 | border-radius: 1rem;
9 | box-shadow: 0 4px 6px rgba(34,25,25,0.3);
10 | margin-bottom: 1rem;
11 | text-align: center; /* Center title and "load more" button */
12 | }
13 |
14 | .row-title {
15 | font-size: 2.5rem;
16 | border-bottom: .1rem solid gray;
17 | }
18 |
19 | .row-stats {
20 | display: flex;
21 | flex-direction: row;
22 | justify-content: center;
23 | flex-wrap: wrap;
24 | }
25 |
26 | .load-more-projects {
27 | display: inline-block;
28 | margin: 0 auto;
29 | margin-bottom: 1rem;
30 | }
31 |
32 | /* Stats */
33 |
34 | .stat, .project {
35 | padding: 1rem;
36 | display: flex;
37 | flex-direction: column;
38 | justify-content: center;
39 | }
40 |
41 | .project {
42 | text-align: left;
43 | }
44 |
45 | .stat-name {
46 | font-size: 1rem;
47 | align-self: center;
48 | }
49 |
50 | .emoji {
51 | font-size: 2rem;
52 | }
53 |
54 | .stat-value {
55 | font-size: 2.5rem;
56 | align-self: center;
57 | }
58 |
59 | /* Projects */
60 |
61 | .project-title {
62 | justify-content: center;
63 | display: flex;
64 | font-size: 1.25rem;
65 | cursor: pointer;
66 | }
67 |
68 | .project-info {
69 | display: flex;
70 | flex-direction: row;
71 | justify-content: center;
72 | }
73 |
74 | .project-thumbnail {
75 | height: 7rem;
76 | width: 9.33rem;
77 | cursor: pointer;
78 | }
79 |
80 | .project-stats {
81 | align-self: center;
82 | }
83 |
84 | .project-stat-value {
85 | float: right;
86 | margin-left: .5rem;
87 | }
88 |
89 | .project-stat-bold {
90 | font-weight: 800;
91 | }
92 |
93 | /* Media */
94 |
95 | /* Desktop */
96 | @media screen and (min-width: 850px) {
97 | .project {
98 | width: 25.1%;
99 | }
100 | .row-stats {
101 | padding-left: 5rem;
102 | padding-right: 5rem;
103 | }
104 | .stat {
105 | padding-left: 3rem;
106 | padding-right: 3rem;
107 | }
108 | }
109 |
110 | /* Mobile landscape */
111 | @media screen and (max-width: 850px) and (min-aspect-ratio: 13/9) {
112 | .project {
113 | width: 25.1%;
114 | }
115 | }
116 |
117 | /*Mobile portrait*/
118 | @media screen and (max-width: 850px) and (max-aspect-ratio: 13/9) {
119 | .row-stats {
120 | flex-direction: column;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ScratchStats.com
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
![]()
38 |
{{user}}
39 |
40 |
45 |
46 |
47 | Join date (your timezone)
48 | Account ID
49 |
50 |
51 |
52 |
Country: {{profile.countryCode}}
53 |
54 | Unread messages
55 | Possible browser
56 | Possible OS
57 | (New?) Scratcher / Scratch Team
58 |
59 |
60 | Shared projects
61 | ❤️ projects
62 | ⭐ projects
63 | Followed users
64 | Joined studios
65 | Followed studios
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | {{statEmojis[stat]}}
79 |
80 |
81 |
82 |
83 | {{statEmojis[stat]}}
84 |
85 |
86 |
87 |
88 | {{statEmojis[stat]}}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
{{scratcher.username}}
96 |
97 | ⬆️
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/404-stats.js:
--------------------------------------------------------------------------------
1 | const countries = {"AD": "Andorra", "AE": "United Arab Emirates", "AF": "Afghanistan", "AG": "Antigua and Barbuda", "AI": "Anguilla", "AL": "Albania", "AM": "Armenia", "AN": "Netherlands Antilles", "AO": "Angola", "AQ": "Antarctica", "AR": "Argentina", "AS": "American Samoa", "AT": "Austria", "AU": "Australia", "AW": "Aruba", "AX": "Aland Islands", "AZ": "Azerbaijan", "BA": "Bosnia and Herzegovina", "BB": "Barbados", "BD": "Bangladesh", "BE": "Belgium", "BF": "Burkina Faso", "BG": "Bulgaria", "BH": "Bahrain", "BI": "Burundi", "BJ": "Benin", "BL": "Saint Barthelemy", "BM": "Bermuda", "BN": "Brunei", "BO": "Bolivia", "BQ": "Bonaire, Sint Eustatius and Saba", "BR": "Brazil", "BS": "Bahamas", "BT": "Bhutan", "BV": "Bouvet Island", "BW": "Botswana", "BY": "Belarus", "BZ": "Belize", "CA": "Canada", "CC": "Cocos (Keeling) Islands", "CD": "Congo, The Democratic Republic of the", "CF": "Central African Republic", "CG": "Congo", "CH": "Switzerland", "CI": "Cote d'Ivoire", "CK": "Cook Islands", "CL": "Chile", "CM": "Cameroon", "CN": "China", "CO": "Colombia", "CR": "Costa Rica", "CU": "Cuba", "CV": "Cape Verde", "CX": "Christmas Island", "CY": "Cyprus", "CZ": "Czech Republic", "DE": "Germany", "DJ": "Djibouti", "DK": "Denmark", "DM": "Dominica", "DO": "Dominican Republic", "DZ": "Algeria", "EC": "Ecuador", "EE": "Estonia", "EG": "Egypt", "EH": "Western Sahara", "ER": "Eritrea", "ES": "Spain", "ET": "Ethiopia", "FI": "Finland", "FJ": "Fiji", "FK": "Falkland Islands (Malvinas)", "FM": "Micronesia, Federated States of", "FO": "Faroe Islands", "FR": "France", "GA": "Gabon", "GB": "United Kingdom", "GD": "Grenada", "GE": "Georgia", "GF": "French Guiana", "GG": "Guernsey", "GH": "Ghana", "GI": "Gibraltar", "GL": "Greenland", "GM": "Gambia", "GN": "Guinea", "GP": "Guadeloupe", "GQ": "Equatorial Guinea", "GR": "Greece", "GS": "South Georgia and the South Sandwich Islands", "GT": "Guatemala", "GU": "Guam", "GW": "Guinea-Bissau", "GY": "Guyana", "HK": "Hong Kong", "HM": "Heard Island and McDonald Islands", "HN": "Honduras", "HR": "Croatia", "HT": "Haiti", "HU": "Hungary", "ID": "Indonesia", "IE": "Ireland", "IL": "Israel", "IM": "Isle of Man", "IN": "India", "IO": "British Indian Ocean Territory", "IQ": "Iraq", "IR": "Iran", "IS": "Iceland", "IT": "Italy", "JE": "Jersey", "JM": "Jamaica", "JO": "Jordan", "JP": "Japan", "KE": "Kenya", "KG": "Kyrgyzstan", "KH": "Cambodia", "KI": "Kiribati", "KM": "Comoros", "KN": "Saint Kitts and Nevis", "KP": "North Korea", "KR": "South Korea", "KW": "Kuwait", "KY": "Cayman Islands", "KZ": "Kazakhstan", "LA": "Laos", "LB": "Lebanon", "LC": "Saint Lucia", "LI": "Liechtenstein", "LK": "Sri Lanka", "LR": "Liberia", "LS": "Lesotho", "LT": "Lithuania", "LU": "Luxembourg", "LV": "Latvia", "LY": "Libya", "MA": "Morocco", "MC": "Monaco", "MD": "Moldova", "ME": "Montenegro", "MF": "Saint Martin", "MG": "Madagascar", "MH": "Marshall Islands", "MK": "Macedonia, The Former Yugoslav Republic of", "ML": "Mali", "MM": "Myanmar", "MN": "Mongolia", "MO": "Macao", "MP": "Northern Mariana Islands", "MQ": "Martinique", "MR": "Mauritania", "MS": "Montserrat", "MT": "Malta", "MU": "Mauritius", "MV": "Maldives", "MW": "Malawi", "MX": "Mexico", "MY": "Malaysia", "MZ": "Mozambique", "NA": "Namibia", "NC": "New Caledonia", "NE": "Niger", "NF": "Norfolk Island", "NG": "Nigeria", "NI": "Nicaragua", "NL": "Netherlands", "NO": "Norway", "NP": "Nepal", "NR": "Nauru", "NU": "Niue", "NZ": "New Zealand", "OM": "Oman", "PA": "Panama", "PE": "Peru", "PF": "French Polynesia", "PG": "Papua New Guinea", "PH": "Philippines", "PK": "Pakistan", "PL": "Poland", "PM": "Saint Pierre and Miquelon", "PN": "Pitcairn", "PR": "Puerto Rico", "PS": "Palestine, State of", "PT": "Portugal", "PW": "Palau", "PY": "Paraguay", "QA": "Qatar", "RE": "Reunion", "RO": "Romania", "RS": "Serbia", "RU": "Russia", "RW": "Rwanda", "SA": "Saudi Arabia", "SB": "Solomon Islands", "SC": "Seychelles", "SD": "Sudan", "SE": "Sweden", "SG": "Singapore", "SH": "Saint Helena", "SI": "Slovenia", "SJ": "Svalbard and Jan Mayen", "SK": "Slovakia", "SL": "Sierra Leone", "SM": "San Marino", "SN": "Senegal", "SO": "Somalia", "SR": "Suriname", "SS": "South Sudan", "ST": "Sao Tome and Principe", "SV": "El Salvador", "SY": "Syria", "SZ": "Swaziland", "TC": "Turks and Caicos Islands", "TD": "Chad", "TF": "French Southern Territories", "TG": "Togo", "TH": "Thailand", "TJ": "Tajikistan", "TK": "Tokelau", "TL": "Timor-Leste", "TM": "Turkmenistan", "TN": "Tunisia", "TO": "Tonga", "TR": "Turkey", "TT": "Trinidad and Tobago", "TV": "Tuvalu", "TW": "Taiwan", "TZ": "Tanzania", "UA": "Ukraine", "UG": "Uganda", "UM": "United States Minor Outlying Islands", "US": "United States", "UY": "Uruguay", "UZ": "Uzbekistan", "VA": "Vatican City", "VC": "Saint Vincent and the Grenadines", "VE": "Venezuela", "VG": "Virgin Islands, British", "VI": "Virgin Islands, U.S.", "VN": "Vietnam", "VU": "Vanuatu", "WF": "Wallis and Futuna", "WS": "Samoa", "XK": "Kosovo", "YE": "Yemen", "YT": "Mayotte", "ZA": "South Africa", "ZM": "Zambia", "ZW": "Zimbabwe"};
2 | const defaultUser = "griffpatch";
3 |
4 | window.onpopstate = event => getStats(location.pathname.substring(1, 99), false, "ignore");
5 |
6 | function init() {
7 | data = new Vue({
8 | el: "#app",
9 | data: function() {return initialData()},
10 | methods: {
11 | n: x => {
12 | return Math.round(x).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
13 | },
14 | reset: function() {
15 | Object.assign(this.$data, initialData());
16 | }
17 | },
18 | created: () => getStats(location.pathname.substring(1, 99), true, "default")
19 | });
20 | }
21 |
22 | function initialData() {
23 | requestsMade = 0;
24 | swal.fire({
25 | toast: true,
26 | position: "center",
27 | showConfirmButton: false,
28 | type: "info",
29 | title: "Loading stats..."
30 | });
31 |
32 | return {
33 | // Static data
34 | "statNames" : ["loves", "favorites", "views", "comments", "liking"],
35 | "statEmojis" : {"loves": "❤️", "favorites": "⭐", "views": "👁️", "comments": "💬", "liking": "👍"},
36 | "statAdjs" : {"loves": "loved", "favorites": "faved", "views": "viewed", "comments": "commented", "liking": "liked"},
37 | "periods": ["days", "weeks", "months", "years"],
38 |
39 | "loaded": false,
40 |
41 | "id": "",
42 | "user": "",
43 | "profile": {
44 | "profilePicUrl": "",
45 | "countryName": "",
46 | "countryCode": "",
47 | "joinDate": "",
48 | "msgCount": 0,
49 | "browser": "?",
50 | "os": "?",
51 | "status": "",
52 | "lastProject": null
53 | },
54 | "activity": {
55 | "loves": 0,
56 | "favorites": 0,
57 | "allFollows": 0,
58 | "usersFollowed": 0,
59 | "studiosFollowed": 0,
60 | "studiosCurated": 0,
61 | "projectsShared": 0,
62 | "lovedProjects": []
63 | },
64 | "joined": {
65 | "days": 0,
66 | "weeks": 0,
67 | "months": 0,
68 | "years": 0
69 | },
70 | "projects": [],
71 | "totals" : {
72 | "loves": 0,
73 | "favorites": 0,
74 | "views": 0,
75 | "comments": 0,
76 | "liking": 0
77 | // Note: liking total isn't shown to the user, but is used to calculate the average
78 | },
79 | "projectsSorted": {
80 | "loves": [],
81 | "favorites": [],
82 | "views": [],
83 | "comments": [],
84 | "liking": []
85 | },
86 | "projectRowShow": {
87 | "loves": 3,
88 | "favorites": 3,
89 | "views": 3,
90 | "comments": 3,
91 | "liking": 3,
92 | "lovedByUser": 3 // "Recently loved projects" row
93 | },
94 |
95 | "exploreScratchers": []
96 | };
97 | }
98 |
99 | function changeUsername() {
100 | swal.fire({
101 | title: "Enter a username",
102 | input: "text",
103 | inputPlaceholder: defaultUser,
104 | inputAttributes: {
105 | autocomplete: "off",
106 | autocapitalize: "off"
107 | }
108 | })
109 | .then(({value}) => {
110 | if(value) {
111 | if(value[0] === "@") value = value.substring(1, 99);
112 | getStats(value, true, "alert");
113 | }
114 | });
115 | }
116 |
117 | async function getStats(username, pushHistory, onError) {
118 | if(username === "404.html") username = defaultUser;
119 | const req = await fetch(`https://api.scratchstats.com/scratch/users/${username}`);
120 | const res = await req.json();
121 | if (!res.code) {
122 | if(data.loaded === true) data.reset();
123 | data.user = res.username;
124 | if(pushHistory) history.pushState({}, "", data.user);
125 | data.id = res.id;
126 | data.profile.profilePicUrl = `https://cdn2.scratch.mit.edu/get_image/user/${data.id}_50x50.png`;
127 | data.profile.status = res.scratchteam === true ? "Scratch Team" : "Scratcher";
128 | data.profile.countryName = res.profile.country;
129 | Object.values(countries).forEach((countryName, index) => {
130 | if(data.profile.countryName === countryName) data.profile.countryCode = Object.keys(countries)[index];
131 | })
132 | const joined = new Date(res.history.joined);;
133 | data.profile.joinDate = joined.toLocaleDateString("en-us", {weekday: "short", year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", second: "2-digit"});
134 | const joinTimestamp = joined.getTime();
135 | const currentTimestamp = Date.now();
136 | const minuteDiff = (currentTimestamp-joinTimestamp)/1000/60;
137 | data.joined.days = minuteDiff/1440;
138 | data.joined.weeks = minuteDiff/10080;
139 | data.joined.months = minuteDiff/43800;
140 | data.joined.years = minuteDiff/525600;
141 | loadProjects(0);
142 | getMsgCount();
143 | getActivityData();
144 | } else {
145 | switch (onError) {
146 | case "default":
147 | getStats(defaultUser, true);
148 | break;
149 | case "ignore":
150 | history.pushState({}, "", data.user);
151 | break;
152 | case "alert":
153 | swal.fire({
154 | toast: true,
155 | position: "center",
156 | showConfirmButton: false,
157 | timer: 4000,
158 | type: "error",
159 | title: "Could not find a Scratcher with that username."
160 | });
161 | break;
162 | }
163 | }
164 | }
165 |
166 | async function loadProjects(offset) {
167 | const limit = 40;
168 | const req = await fetch(`https://api.scratchstats.com/scratch/users/${data.user}/projects/?limit=${limit}&offset=${offset}`);
169 | if(req.status === 200) {
170 | const res = await req.json();
171 |
172 | // Exactly returned 40 projects?
173 | if(res.length === limit) loadProjects(offset+limit);
174 |
175 | // Returned at least one project? Save last project ID
176 | if(res.length !== 0) data.profile.lastProject = res[res.length-1].id;
177 |
178 | // No projects at all?
179 | if(res.length === 0 && offset === 0) requestMade();
180 |
181 | // Used offset but no new projects were returned
182 | if(res.length === 0 && offset !== 0) loadedProjects();
183 |
184 | res.forEach((project, index) => {
185 | project.stats.liking = (project.stats.loves/project.stats.views)*100;
186 | data.projects.push(project);
187 | data.statNames.forEach((stat, index) => data.totals[stat] += project.stats[stat]);
188 | });
189 | if(res.length !== limit) loadedProjects();
190 | }
191 | }
192 |
193 | function loadedProjects() {
194 | requestMade();
195 | if(data.profile.lastProject) getUserAgent();
196 | data.statNames.forEach((stat, index) => {
197 | const projectArray = data.projects.slice();
198 | projectArray.sort((a, b) => {
199 | return b.stats[stat] - a.stats[stat];
200 | });
201 | data.projectsSorted[stat] = projectArray;
202 | });
203 | }
204 |
205 | async function getMsgCount() {
206 | const req = await fetch(`https://api.scratchstats.com/scratch/users/${data.user}/messages/count`);
207 | if(req.status === 200) {
208 | const res = await req.json();
209 | data.profile.msgCount = res.count;
210 | }
211 | }
212 |
213 | async function getUserAgent() {
214 | const req = await fetch(`https://projects.scratch.mit.edu/${data.profile.lastProject}`);
215 | if (req.status === 200) {
216 | try {
217 | const res = await req.json();
218 | const userAgent = res.meta ? res.meta.agent : (res.info ? res.info.userAgent : "");
219 | if(userAgent) {
220 | const browser = new UAParser(userAgent).getBrowser();
221 | const os = new UAParser(userAgent).getOS();
222 | if(browser.name) data.profile.browser = browser.name;
223 | if(os.name) data.profile.os = `${os.name} ${os.version}`;
224 | }
225 | } catch(error) {
226 | console.log("Couldn't get user agent")
227 | }
228 | }
229 | }
230 |
231 | async function getActivityData() {
232 | const matches = {
233 | "loves": "icon-xs black love",
234 | "favorites": "icon-xs black favorite",
235 | "allFollows": "is now following", // not shown to the user
236 | "studiosFollowed": "is now following the studio",
237 | "usersFollowed": null, // is calculated differently - it's here so we can use the same loop
238 | "studiosCurated": "became a curator of",
239 | "projectsShared": "shared the project"
240 | };
241 | const req = await fetch(`https://api.scratchstats.com/scratch/users/${data.user}/activity`);
242 | requestMade();
243 | if(req.status === 200) {
244 | const res = await req.text();
245 | const parser = new DOMParser();
246 | const html = parser.parseFromString(res, 'text/html');
247 | const activities = html.getElementsByTagName("li");
248 |
249 | for(activity of activities) {
250 | if(activity.getElementsByClassName("time")[0].innerText.includes("year")) continue; // Older than 12 months
251 | if(activity.innerHTML.includes("icon-xs black love")) {
252 | const projectID = activity.getElementsByTagName("a")[0].href.replace(/\D/g,'');
253 | const projectTitle = activity.getElementsByTagName("a")[0].innerText;
254 | data.activity.lovedProjects.push({
255 | "id": projectID,
256 | "title": projectTitle
257 | });
258 | }
259 | Object.keys(matches).forEach((item, index) => {
260 | if(activity.innerHTML.includes(matches[item])) data.activity[item] += 1;
261 | });
262 | }
263 | data.activity.usersFollowed = data.activity.allFollows - data.activity.studiosFollowed;
264 | if(data.activity.usersFollowed === 20) data.activity.usersFollowed = "20+";
265 |
266 | Object.keys(matches).forEach((item, index) => {
267 | if(data.activity[item] === 20) data.activity[item] = "20+"
268 | });
269 | }
270 | }
271 |
272 | async function setScratchersToExplore() {
273 | const req = await fetch(`https://api.scratchstats.com/scratch/explore/projects?limit=6&offset=${Math.round(Math.random()*(200-0)+0)}&language=en&mode=trending&q=*`);
274 | const res = await req.json();
275 | for(project of res) {
276 | const scratcher = {};
277 | scratcher.username = project.author.username;
278 | scratcher.id = project.author.id;
279 | data.exploreScratchers.push(scratcher);
280 | }
281 | }
282 |
283 | //
284 |
285 | function requestMade() {
286 | requestsMade++;
287 | if(requestsMade === 2) {
288 | swal.close();
289 | data.loaded = true;
290 | setScratchersToExplore();
291 | }
292 | }
293 |
294 | function copyLink() {
295 | const el = document.createElement("textarea");
296 | el.value = `https://scratchstats.com/${data.user}`;
297 | document.body.appendChild(el);
298 | el.select();
299 | document.execCommand("copy");
300 | document.body.removeChild(el);
301 | swal.fire({
302 | toast: true,
303 | position: "center",
304 | showConfirmButton: false,
305 | timer: 2000,
306 | type: "success",
307 | title: "Copied to clipboard"
308 | });
309 | }
310 |
--------------------------------------------------------------------------------