mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-29 15:27:01 -05:00
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:
335
frontend/src/components/auth/SignupModal.vue
Normal file
335
frontend/src/components/auth/SignupModal.vue
Normal 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>
|
||||
Reference in New Issue
Block a user