Skip to content

Commit

Permalink
Merge pull request #21 from Furiiku/reviews-and-store_fix-feature
Browse files Browse the repository at this point in the history
Find reviews and clear missing stores features
  • Loading branch information
Furiiku authored Oct 1, 2024
2 parents 46f8740 + fe66f63 commit 1917d70
Show file tree
Hide file tree
Showing 11 changed files with 618 additions and 61 deletions.
95 changes: 94 additions & 1 deletion src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { type Achievement, type AchievementsResponse } from './apiTypes/achievements'
import { type MeResponse } from './apiTypes/me'
import { type Order, type OrderResponse } from './apiTypes/order'
import { type Review, type ReviewResponse } from './apiTypes/review'
import { type DeleteStoreResponse } from './apiTypes/starred-store'
import { type Drop, type SupplyDropResponse } from './apiTypes/supplyDrop'
import { getCachedPromise } from './promiseCache'

export const fetchAPI = async <ExpectedType = unknown> (
uri: string,
params?: Record<string, string | number>,
method: string = 'GET',
): Promise<ExpectedType> => {
const url = new URL(uri)
if (params) {
Expand All @@ -17,7 +20,10 @@ export const fetchAPI = async <ExpectedType = unknown> (
})
}

return await fetch(url.toString())
return await fetch(url.toString(), {
method,
signal: AbortSignal.timeout(10000),
})
.then(async (response) => {
// The API call was successful!
return await response.json()
Expand Down Expand Up @@ -63,6 +69,70 @@ export const fetchOrders = async (whId: number): Promise<Order[]> => {
})
}

function updateProgress (current: number, total: number): void {
const progressBar = document.getElementById('review-progress-bar') as HTMLElement
const progressText = document.getElementById('review-progress-text') as HTMLElement

if (progressBar && progressText) {
const percentage = (current / total) * 100
progressBar.style.width = `${percentage}%`
progressText.innerText = `${current} av ${total}`
} else {
console.error('Could not find progress bar!')
}
}

export interface ProductReview {
product: number
review: Review | undefined
}
export const fetchUserReviewsFresh = async (whId: number): Promise<ProductReview[]> => {
const handledProducts = [] as number[]
const userReviews = []
const orders = await fetchOrders(whId)
let current = 0
const total = orders.flatMap(order => order.rows).length

for (const order of orders) {
for (const item of order.rows) {
updateProgress(current++, total)
if (handledProducts.includes(item.product.id)) continue
handledProducts.push(item.product.id)

const id = item.product.id
const productReviews = await fetchProductReviews(id)
const userProductReview = productReviews.find(review => {
if (review.isAnonymous) return false

try {
return review.user.id === whId
} catch (error) {
console.error('Error accessing review:', review, error)
return false
}
})
if (userProductReview) {
console.log('Found a review')
}
userReviews.push({
product: id,
review: userProductReview,
})
}
}

return userReviews
}

export const fetchUserReviews = async (whId: number): Promise<ProductReview[]> => {
return await getCachedPromise({
key: `${whId}-reviews`,
fn: async () => {
return await fetchUserReviewsFresh(whId)
},
})
}

export const fetchAchievements = async (whId: number): Promise<Achievement[]> => {
const data = await fetchAPI<AchievementsResponse>(`https://www.webhallen.com/api/user/${encodeURIComponent(whId)}/achievements`)
return data.achievements
Expand Down Expand Up @@ -115,3 +185,26 @@ export async function fetchProductData (productId: string | number): Promise<Pro
return null
}
}

export async function fetchProductReviews (productId: string | number): Promise<Review[]> {
let page = 1
const reviews = []

while (true) {
const params = { page }
const data = await fetchAPI<ReviewResponse>(`https://www.webhallen.com/api/reviews?products[0]=${productId}&sortby=latest`, params)
if (data.reviews.length === 0) break
reviews.push(...data.reviews)
page++
}

return reviews
}

export async function deleteFavoriteStores (): Promise<null> {
for (let i = 0; i <= 40; i++) {
console.log(`Tar bort butik ${i} från favoriter.`)
await fetchAPI<DeleteStoreResponse>(`https://www.webhallen.com/api/starred-store/${i}`, {}, 'DELETE')
}
return null
}
39 changes: 39 additions & 0 deletions src/lib/apiTypes/review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable no-multi-spaces */
/* eslint-disable @typescript-eslint/key-spacing */

import { type Avatar } from './me'
import { type Product } from './order'

export interface ReviewResponse {
reviews: Review[]
hypes: unknown[]
totalReviewCount: number
totalHypeCount: number
reviewsPerPage: number
ratingReviewCount: number
currentReviewCount: number
}

export interface Review {
id: number
text: string
rating: number
upvotes: number
downvotes: number
verifiedPurchase: boolean
createdAt: number
isAnonymous: boolean
isEmployee: boolean
product: Product
isHype: boolean
user: ReviewUser
}

export interface ReviewUser {
id: number
username: string
isPublicProfile: boolean
knighthood: number[]
rankLevel: number
avatar: Avatar
}
3 changes: 3 additions & 0 deletions src/lib/apiTypes/starred-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface DeleteStoreResponse {
status: string
}
59 changes: 59 additions & 0 deletions src/lib/builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export function findInjectPath (paths: string[]): HTMLElement | null {
let dom = null
paths.forEach(path => {
const d = document.querySelector(path)
if (d) {
dom = d
}
})

return dom
}

export function addDataToDiv (headerText: string, domObject: Element): HTMLDivElement {
const div = document.createElement('div')
div.className = 'order my-4'

const table = document.createElement('table')
table.className = 'table table-condensed'

const tbody = document.createElement('tbody')

const tr = document.createElement('tr')
tr.className = 'order-id-wrap'

const td = document.createElement('td')
td.textContent = headerText

tr.appendChild(td)
tbody.appendChild(tr)
table.appendChild(tbody)
div.appendChild(table)

const div1 = document.createElement('div')
const div2 = document.createElement('div')
const orderProgression = document.createElement('div')
const innerContainer = document.createElement('div')
const orderStatusEvent = document.createElement('div')
const icon = document.createElement('div')
const header = document.createElement('h3')
const secondary = document.createElement('div')

div1.appendChild(div2)
div2.appendChild(orderProgression)
orderProgression.appendChild(innerContainer)
innerContainer.appendChild(orderStatusEvent)
orderStatusEvent.appendChild(icon)
orderStatusEvent.appendChild(header)
orderStatusEvent.appendChild(secondary)
secondary.appendChild(domObject)

header.className = 'level-two-heading'
icon.className = 'icon'

header.textContent = ''

div.appendChild(div1)

return div
}
38 changes: 38 additions & 0 deletions src/lib/datetime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export function timeAgo (unixTimestamp: number): string {
const now = Math.floor(Date.now() / 1000)
const secondsAgo = now - unixTimestamp

const timeUnits = [
{ singular: 'år', plural: 'år', seconds: 365 * 24 * 60 * 60 },
{ singular: 'månad', plural: 'månader', seconds: 30 * 24 * 60 * 60 },
{ singular: 'dag', plural: 'dagar', seconds: 24 * 60 * 60 },
{ singular: 'timma', plural: 'timmar', seconds: 60 * 60 },
{ singular: 'minut', plural: 'minuter', seconds: 60 },
]

for (const { singular, plural, seconds } of timeUnits) {
const count = Math.floor(secondsAgo / seconds)
if (count >= 1) {
return count === 1 ? `1 ${singular} sedan` : `${count} ${plural} sedan`
}
}

return 'Just nu'
}

export function unixTimestampToLocale (unixTimestamp: number): string {
const date = new Date(unixTimestamp * 1000)
const locale = navigator.language || 'sv-SE'

const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short',
} as Intl.DateTimeFormatOptions

return new Intl.DateTimeFormat(locale, options).format(date)
}
15 changes: 15 additions & 0 deletions src/reducers/reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type ProductReview } from '../lib/api'

export function getPostedReviews (reviews: ProductReview[]): ProductReview[] {
return reviews.filter(orderReview => orderReview.review !== undefined)
.sort((a, b) => {
if (a.review && b.review) {
return b.review.createdAt - a.review.createdAt
}
return 0
})
}

export function getProductsWithoutReviews (reviews: ProductReview[]): ProductReview[] {
return reviews.filter(orderReview => orderReview.review === undefined)
}
60 changes: 60 additions & 0 deletions src/renderers/favstores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { deleteFavoriteStores } from '../lib/api'

async function _clearAllStores (event: MouseEvent): Promise<void> {
event.preventDefault()
await deleteFavoriteStores()
location.reload()
}

function observeDOM (): void {
const targetNode = document.body

const config = { childList: true, subtree: true }

const callback = function (mutationsList: MutationRecord[], observer: MutationObserver): void {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
const addedNode = node as unknown as HTMLElement
if (addedNode.className && addedNode.className === 'stores-map') {
const storelist = addedNode.querySelector('.list-group') as HTMLDivElement
if (storelist) {
console.log('Found store list', storelist)
const li = document.createElement('li')
li.className = 'list-group-item store-list-item'
const label = document.createElement('label')
label.className = 'stock-favorite'
label.title = 'Rensa'
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
const span1 = document.createElement('span')
span1.textContent = 'Rensa alla butiker'
const link = document.createElement('a')
link.href = '#'
link.className = 'store-info'
link.addEventListener('click', (event) => { _clearAllStores(event).catch(() => { }) })
const span2 = document.createElement('span')
span2.className = 'store-location'
span2.textContent = 'Löser problem med butiker som inte går att ta bort då de försvunnit'
link.appendChild(span1)
link.appendChild(span2)
li.appendChild(label)
li.appendChild(link)
storelist.prepend(li)
}
}
})
}
}
}

const observer = new MutationObserver(callback)
observer.observe(targetNode, config)
}

let renderedAlready = false
export const renderClearFavoriteStoresUtility = (): void => {
if (renderedAlready) return
observeDOM()
renderedAlready = true
}
Loading

0 comments on commit 1917d70

Please sign in to comment.