mirror of
https://github.com/maciejpedzich/racemash.git
synced 2024-11-27 16:15:47 +01:00
Code up an algorithm for picking random photos
This commit is contained in:
parent
3cd16b764b
commit
7bf979101f
12
src/composables/useRandomNumber.ts
Normal file
12
src/composables/useRandomNumber.ts
Normal 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);
|
||||
};
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user