Code up an algorithm for picking random photos

This commit is contained in:
Maciej Pędzich 2023-05-30 17:55:56 +02:00
parent 3cd16b764b
commit 7bf979101f
2 changed files with 140 additions and 26 deletions

View File

@ -0,0 +1,12 @@
// Why not a standalone randomNumber function?
// I just couldn't be bothered creating a separate utils folder or installing a new package.
export function useRandomNumber() {
// Shamelessly stolen from:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive
return (min: number, max: number) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
};
}

View File

@ -1,32 +1,129 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { storage } from '@/appwrite';
import { computed, onMounted, ref, reactive } from 'vue';
import { Query } from 'appwrite';
import { databases } from '@/appwrite';
import { useAuth } from '@/composables/useAuth';
import { useSnackbar } from '@/composables/useSnackbar';
import { useRandomNumber } from '@/composables/useRandomNumber';
import { Vote } from '@/models/vote';
import { Photo } from '@/models/photo';
import { PhotoAppearanceCount } from '@/models/photoAppearanceCount';
const { user } = useAuth();
const { showSnackbar } = useSnackbar();
const randomNumber = useRandomNumber();
const isLoading = ref(true);
const testImages = ref<string[]>([]);
const photoAppearanceCount = reactive<Record<string, number>>({});
const photosInCurrentVote = ref<Photo[]>([]);
function randomNum(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
}
// Get IDs of photos that have appeared in votes between all the other photos
const idsOfPhotosPairedWithAll = computed(() =>
Object.entries(JSON.parse(JSON.stringify(photoAppearanceCount)))
// You shouldn't be able to vote between two identical photos.
// That's why we need to subtract one from the number of photos.
.filter(([, count]) => count === import.meta.env.VITE_NUMBER_OF_PHOTOS - 1)
.map(([photoId]) => photoId)
);
const loadPhotosAppearanceCount = async () => {
const { documents: photoAppearanceCountEntries } =
await databases.listDocuments<PhotoAppearanceCount>(
import.meta.env.VITE_DATABASE_ID,
import.meta.env.VITE_PHOTO_APPEARANCE_COUNT_COLLECTION_ID,
[Query.equal('voterId', user.value?.$id as string)]
);
const appearanceCountRecord = photoAppearanceCountEntries.reduce(
(record, { photoId, count }) => {
record[photoId] = count;
return record;
},
{} as Record<string, number>
);
Object.assign(photoAppearanceCount, appearanceCountRecord);
};
const pickRandomPhotosAndCreateVote = async () => {
// Pick a random photo that hasn't been paired with every other photo in the user's votes
const { documents: allPhotos } = await databases.listDocuments<Photo>(
import.meta.env.VITE_DATABASE_ID,
import.meta.env.VITE_PHOTOS_COLLECTION_ID
// [
// Query.select(['photoId', 'url', 'altText']) -> Unsupported in Appwrite v1.1.2,
// Query.notEqual('photoId', idsOfPhotosPairedWithAll.value) -> Throws ambiguous Server Error
// ]
);
const photo1Candidates = allPhotos.filter(
({ $id }) => !idsOfPhotosPairedWithAll.value.includes($id)
);
const maxPhoto1Index =
import.meta.env.VITE_NUMBER_OF_PHOTOS -
idsOfPhotosPairedWithAll.value.length -
1;
const photo1 = photo1Candidates[randomNumber(0, maxPhoto1Index)];
// Get IDs of photos that have already been paired with photo1 in user's votes
const idsOfPhotosPairedWithPhoto1 =
// Select votes where photo1Id is equal to photo1's ID
(
await databases.listDocuments<Vote>(
import.meta.env.VITE_DATABASE_ID,
import.meta.env.VITE_VOTES_COLLECTION_ID,
[
// Query.select(['photo2Id']),
Query.equal('voterId', [user.value?.$id as string]),
Query.search('photos', photo1.$id)
]
)
).documents.flatMap((doc) =>
Object.entries(doc)
.filter(([key]) => key === 'photos')
.map(([, arrayOfPhotoIds]) =>
arrayOfPhotoIds.filter((id: string) => id !== photo1.$id)
)
);
// Pick a random photo that isn't photo1 and that hasn't been paired with it in any of the user's votes
const photo2IdsToExclude = [
photo1.$id,
...idsOfPhotosPairedWithPhoto1,
...idsOfPhotosPairedWithAll.value
];
const { documents: photo2Candidates } = await databases.listDocuments<Photo>(
import.meta.env.VITE_DATABASE_ID,
import.meta.env.VITE_PHOTOS_COLLECTION_ID,
[Query.notEqual('$id', photo2IdsToExclude)]
);
const maxPhoto2Index =
import.meta.env.VITE_NUMBER_OF_PHOTOS - photo2IdsToExclude.length - 1;
const photo2 = photo2Candidates[randomNumber(0, maxPhoto2Index)];
photosInCurrentVote.value.push(photo1, photo2);
};
onMounted(async () => {
try {
const { files } = await storage.listFiles('photos');
const urls = files.map(
({ $id }) => storage.getFileView('photos', $id).href
);
console.log(urls.join('\n\n'));
for (let i = 0; i < 2; i++) {
const randomUrlIndex = randomNum(0, urls.length - 1);
testImages.value.push(urls[randomUrlIndex]);
urls.splice(randomUrlIndex, 1);
}
await loadPhotosAppearanceCount();
await pickRandomPhotosAndCreateVote();
} catch (error) {
console.error(error);
showSnackbar({
status: 'error',
message: 'Failed to load photos'
});
} finally {
isLoading.value = false;
}
@ -46,18 +143,23 @@ onMounted(async () => {
class="py-lg-6 py-3 d-flex flex-lg-row flex-lg-row flex-column align-center"
>
<div
v-for="(imgUrl, index) in testImages"
:key="imgUrl"
v-for="(photo, index) in photosInCurrentVote"
:key="photo.$id"
class="d-flex flex-column align-center px-5 py-4"
>
<v-img :src="imgUrl" max-width="480" :aspect-ratio="16 / 9" />
<v-img
:src="photo.url"
:alt="photo.altText"
max-width="480"
:aspect-ratio="16 / 9"
/>
<p class="mt-2 text-h6">Photo {{ index + 1 }}</p>
</div>
</div>
<div id="vote-btns" class="mb-4 d-flex justify-center flex-wrap">
<v-btn size="large">Photo 1</v-btn>
<v-btn size="large">Photo 2</v-btn>
<v-btn size="large">I can't decide</v-btn>
<v-btn :disabled="isLoading" size="large">Photo 1</v-btn>
<v-btn :disabled="isLoading" size="large">Photo 2</v-btn>
<v-btn :disabled="isLoading" size="large">I can't decide</v-btn>
</div>
</section>
</template>