feat: major API restructure and Vue.js frontend integration

- Centralize API endpoints in dedicated api app with v1 versioning
- Remove individual API modules from parks and rides apps
- Add event tracking system with analytics functionality
- Integrate Vue.js frontend with Tailwind CSS v4 and TypeScript
- Add comprehensive database migrations for event tracking
- Implement user authentication and social provider setup
- Add API schema documentation and serializers
- Configure development environment with shared scripts
- Update project structure for monorepo with frontend/backend separation
This commit is contained in:
pacnpal
2025-08-24 16:42:20 -04:00
parent 92f4104d7a
commit e62646bcf9
127 changed files with 27734 additions and 1867 deletions

View File

@@ -0,0 +1,335 @@
<template>
<AuthModal :show="show" title="Create Account" @close="$emit('close')">
<!-- Social Login Buttons -->
<div class="space-y-3 mb-6">
<button
v-for="provider in socialProviders"
:key="provider.id"
@click="loginWithProvider(provider)"
class="w-full flex items-center justify-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
<component :is="getProviderIcon(provider.id)" class="w-5 h-5 mr-3" />
Continue with {{ provider.name }}
</button>
</div>
<!-- Divider -->
<div v-if="socialProviders.length > 0" class="relative mb-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
Or create account with email
</span>
</div>
</div>
<!-- Signup Form -->
<form @submit.prevent="handleSignup" class="space-y-6">
<!-- Error Messages -->
<div
v-if="authError"
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"
>
<div class="text-sm text-red-600 dark:text-red-400">{{ authError }}</div>
</div>
<!-- Name Fields -->
<div class="grid grid-cols-2 gap-4">
<div>
<label
for="signup-first-name"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
First Name
</label>
<input
id="signup-first-name"
v-model="form.first_name"
type="text"
autocomplete="given-name"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="First name"
/>
</div>
<div>
<label
for="signup-last-name"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Last Name
</label>
<input
id="signup-last-name"
v-model="form.last_name"
type="text"
autocomplete="family-name"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Last name"
/>
</div>
</div>
<!-- Username Field -->
<div>
<label
for="signup-username"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Username
</label>
<input
id="signup-username"
v-model="form.username"
type="text"
required
autocomplete="username"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Choose a username"
/>
</div>
<!-- Email Field -->
<div>
<label
for="signup-email"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Email Address
</label>
<input
id="signup-email"
v-model="form.email"
type="email"
required
autocomplete="email"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Enter your email"
/>
</div>
<!-- Password Field -->
<div>
<label
for="signup-password"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Password
</label>
<div class="relative">
<input
id="signup-password"
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
required
autocomplete="new-password"
class="w-full px-4 py-3 pr-12 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Create a password"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<EyeIcon v-if="showPassword" class="h-5 w-5" />
<EyeSlashIcon v-else class="h-5 w-5" />
</button>
</div>
</div>
<!-- Confirm Password Field -->
<div>
<label
for="signup-password-confirm"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Confirm Password
</label>
<div class="relative">
<input
id="signup-password-confirm"
v-model="form.password_confirm"
:type="showPasswordConfirm ? 'text' : 'password'"
required
autocomplete="new-password"
class="w-full px-4 py-3 pr-12 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Confirm your password"
/>
<button
type="button"
@click="showPasswordConfirm = !showPasswordConfirm"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<EyeIcon v-if="showPasswordConfirm" class="h-5 w-5" />
<EyeSlashIcon v-else class="h-5 w-5" />
</button>
</div>
</div>
<!-- Terms and Privacy -->
<div class="flex items-start">
<input
id="signup-terms"
v-model="form.agreeToTerms"
type="checkbox"
required
class="h-4 w-4 mt-1 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
<label for="signup-terms" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
I agree to the
<a
href="/terms"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300"
>
Terms of Service
</a>
and
<a
href="/privacy"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300"
>
Privacy Policy
</a>
</label>
</div>
<!-- Submit Button -->
<div>
<button
type="submit"
:disabled="isLoading"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<svg
v-if="isLoading"
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 714 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{{ isLoading ? 'Creating Account...' : 'Create Account' }}
</button>
</div>
</form>
<!-- Sign In Link -->
<div class="mt-6 text-sm text-center">
<p class="text-gray-600 dark:text-gray-400">
Already have an account?
<button
@click="$emit('showLogin')"
class="ml-1 font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300"
>
Sign in
</button>
</p>
</div>
</AuthModal>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/outline'
import AuthModal from './AuthModal.vue'
import GoogleIcon from '../icons/GoogleIcon.vue'
import DiscordIcon from '../icons/DiscordIcon.vue'
import { useAuth } from '@/composables/useAuth'
import type { SocialAuthProvider } from '@/types'
interface Props {
show: boolean
}
defineProps<Props>()
const emit = defineEmits<{
close: []
showLogin: []
success: []
}>()
const auth = useAuth()
const { isLoading, authError } = auth
const showPassword = ref(false)
const showPasswordConfirm = ref(false)
const socialProviders = ref<SocialAuthProvider[]>([])
const form = ref({
first_name: '',
last_name: '',
username: '',
email: '',
password: '',
password_confirm: '',
agreeToTerms: false,
})
// Load social providers on mount
onMounted(async () => {
socialProviders.value = await auth.getSocialProviders()
})
const handleSignup = async () => {
try {
await auth.signup({
first_name: form.value.first_name,
last_name: form.value.last_name,
username: form.value.username,
email: form.value.email,
password: form.value.password,
password_confirm: form.value.password_confirm,
})
// Clear form
form.value = {
first_name: '',
last_name: '',
username: '',
email: '',
password: '',
password_confirm: '',
agreeToTerms: false,
}
emit('success')
emit('close')
} catch (error) {
// Error is handled by the auth composable
console.error('Signup failed:', error)
}
}
const loginWithProvider = (provider: SocialAuthProvider) => {
// Redirect to Django allauth provider URL
window.location.href = provider.login_url
}
const getProviderIcon = (providerId: string) => {
switch (providerId) {
case 'google':
return GoogleIcon
case 'discord':
return DiscordIcon
default:
return 'div'
}
}
</script>