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>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, reactive } from 'vue';
|
||||||
import { storage } from '@/appwrite';
|
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 isLoading = ref(true);
|
||||||
const testImages = ref<string[]>([]);
|
const photoAppearanceCount = reactive<Record<string, number>>({});
|
||||||
|
const photosInCurrentVote = ref<Photo[]>([]);
|
||||||
|
|
||||||
function randomNum(min: number, max: number) {
|
// Get IDs of photos that have appeared in votes between all the other photos
|
||||||
min = Math.ceil(min);
|
const idsOfPhotosPairedWithAll = computed(() =>
|
||||||
max = Math.floor(max);
|
Object.entries(JSON.parse(JSON.stringify(photoAppearanceCount)))
|
||||||
return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive
|
// 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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const { files } = await storage.listFiles('photos');
|
await loadPhotosAppearanceCount();
|
||||||
const urls = files.map(
|
await pickRandomPhotosAndCreateVote();
|
||||||
({ $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);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
showSnackbar({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Failed to load photos'
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
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"
|
class="py-lg-6 py-3 d-flex flex-lg-row flex-lg-row flex-column align-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(imgUrl, index) in testImages"
|
v-for="(photo, index) in photosInCurrentVote"
|
||||||
:key="imgUrl"
|
:key="photo.$id"
|
||||||
class="d-flex flex-column align-center px-5 py-4"
|
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>
|
<p class="mt-2 text-h6">Photo {{ index + 1 }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="vote-btns" class="mb-4 d-flex justify-center flex-wrap">
|
<div id="vote-btns" class="mb-4 d-flex justify-center flex-wrap">
|
||||||
<v-btn size="large">Photo 1</v-btn>
|
<v-btn :disabled="isLoading" size="large">Photo 1</v-btn>
|
||||||
<v-btn size="large">Photo 2</v-btn>
|
<v-btn :disabled="isLoading" size="large">Photo 2</v-btn>
|
||||||
<v-btn size="large">I can't decide</v-btn>
|
<v-btn :disabled="isLoading" size="large">I can't decide</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user